@divriots/jampack 0.8.0 → 0.8.1

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 CHANGED
@@ -43,8 +43,8 @@ Optimizes static websites for best user experience and best Core Web Vitals scor
43
43
  - [Optimize assets above-the-fold](https://jampack.divriots.com/features/optimize-above-the-fold)
44
44
  - [Embed small images](https://jampack.divriots.com/features/embed-small-images)
45
45
  - [Compress all assets](https://jampack.divriots.com/features/compress-all)
46
- - [Fixes things automatically](https://jampack.divriots.com/features/autofixes)
47
- - [Raises warnings](https://jampack.divriots.com/features/warnings)
46
+ - [Fix things automatically](https://jampack.divriots.com/features/autofixes)
47
+ - [Raise warnings](https://jampack.divriots.com/features/warnings)
48
48
 
49
49
  ## Quick use
50
50
 
@@ -9,7 +9,6 @@ export type Image = {
9
9
  };
10
10
  export type ImageOutputOptions = {
11
11
  resize?: sharp.ResizeOptions;
12
- toFormat?: 'webp' | 'avif' | 'png' | 'jpeg' | 'unchanged';
13
- progressive?: boolean;
12
+ toFormat?: 'webp' | 'avif' | 'png' | 'jpeg' | 'jpeg+progressive' | 'unchanged';
14
13
  };
15
14
  export declare function compressImage(data: Buffer, options: ImageOutputOptions): Promise<Image | undefined>;
@@ -79,13 +79,15 @@ export async function compressImage(data, options) {
79
79
  // To format
80
80
  switch (toFormat) {
81
81
  case 'jpeg':
82
- sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: options.progressive } ||
83
- {});
82
+ sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options } || {});
83
+ outputFormat = 'jpg';
84
+ break;
85
+ case 'jpeg+progressive':
86
+ sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
84
87
  outputFormat = 'jpg';
85
88
  break;
86
89
  case 'png':
87
- sharpFile = sharpFile.png({ ...config.image.png.options, progressive: options.progressive } ||
88
- {});
90
+ sharpFile = sharpFile.png({ ...config.image.png.options } || {});
89
91
  outputFormat = 'png';
90
92
  break;
91
93
  case 'webp':
package/dist/optimize.js CHANGED
@@ -79,7 +79,7 @@ function getTheFold($) {
79
79
  return theFolds[theFolds.length - 1].startIndex;
80
80
  }
81
81
  async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
82
- var _a;
82
+ var _a, _b;
83
83
  const img = $(imgElement);
84
84
  /*
85
85
  * Attribute 'src'
@@ -161,9 +161,24 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
161
161
  let newImage;
162
162
  const isInPicture = (_a = img.parent()) === null || _a === void 0 ? void 0 : _a.is('picture');
163
163
  let srcToFormat = 'unchanged';
164
- if (isInPicture || isAboveTheFold) {
164
+ if (isAboveTheFold) {
165
+ // Above the fold, nothing beats progressive JPEG for LCP
166
+ // but it's not transparent
167
+ if ((_b = (await originalImage.getImageMeta())) === null || _b === void 0 ? void 0 : _b.hasAlpha) {
168
+ // TODO some images have Alpha metadata but have 0 alpha pixels
169
+ // We should be able to detect these to take advantage of Progressive JPEG
170
+ srcToFormat =
171
+ (await originalImage.getMime()) === 'image/avif' ||
172
+ (await originalImage.getMime()) === 'image/webp'
173
+ ? 'unchanged'
174
+ : 'webp';
175
+ }
176
+ else {
177
+ srcToFormat = 'jpeg+progressive';
178
+ }
179
+ }
180
+ else if (isInPicture) {
165
181
  // srcToFormat should not change if in picture
166
- // If above the fold, we will create progressive images
167
182
  }
168
183
  else {
169
184
  // Let's go to avif -> avif , webp -> webp, else * -> webp
@@ -178,10 +193,10 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
178
193
  $state.optimizedFiles.add(originalImage.filePathAbsolute);
179
194
  newImage = await compressImage(await originalImage.getData(), {
180
195
  toFormat: srcToFormat,
181
- progressive: isAboveTheFold,
182
196
  });
183
197
  if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
184
- (newImage.data.length < (await originalImage.getLen()) || isAboveTheFold) // Always take the new image above to fold to benefit from progressive
198
+ (newImage.data.length < (await originalImage.getLen()) ||
199
+ srcToFormat === 'jpeg+progressive') // Progressive override all even if worse
185
200
  ) {
186
201
  // Do we need to add an new extension?
187
202
  const newExtension = `.${newImage.format}`;
@@ -275,7 +290,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
275
290
  // of the srcset
276
291
  }
277
292
  else {
278
- const new_srcset = await generateSrcSet(htmlfile, originalImage, attrib_src, w, h, true, imageLengthToBeatInSrcSet, { toFormat: srcToFormat, progressive: isAboveTheFold });
293
+ const new_srcset = await generateSrcSet(htmlfile, originalImage, img.attr('src'), imageLengthToBeatInSrcSet, { toFormat: srcToFormat });
279
294
  if (new_srcset !== null) {
280
295
  img.attr('srcset', new_srcset);
281
296
  }
@@ -289,68 +304,82 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
289
304
  */
290
305
  if (isInPicture) {
291
306
  const picture = img.parent();
292
- // List of possible sources to generate
293
- //
294
- const sourcesToGenerate = [];
295
- // Only try to generate better images
296
- // TODO generate more compatible formats (avif => webp/jpeg/png)
297
- //
298
- switch (await originalImage.getMime()) {
299
- case 'image/jpeg':
300
- case 'image/png':
301
- sourcesToGenerate.push({
302
- mime: 'image/webp',
303
- format: 'webp',
304
- });
305
- case 'image/webp':
306
- sourcesToGenerate.push({
307
- mime: 'image/avif',
308
- format: 'avif',
309
- });
310
- case 'image/avif':
311
- // Nothing better
312
- break;
307
+ if (isAboveTheFold && srcToFormat === 'jpeg+progressive') {
308
+ // We don't need anything else than jpeg progressive above the fold
309
+ const nodes = picture.children(`source`);
310
+ nodes.remove();
313
311
  }
314
- for (const s of sourcesToGenerate) {
315
- const sourceWithThisMimeType = picture.children(`source[type="${s.mime}"]`);
316
- if (sourceWithThisMimeType.length > 0) {
317
- // Ignore the creation of sources that already exist
318
- continue;
312
+ else {
313
+ // List of possible sources to generate
314
+ //
315
+ const sourcesToGenerate = [];
316
+ // Only try to generate better images
317
+ // TODO generate more compatible formats (avif => webp/jpeg/png)
318
+ //
319
+ switch (await originalImage.getMime()) {
320
+ case 'image/jpeg':
321
+ case 'image/png':
322
+ sourcesToGenerate.push({
323
+ mime: 'image/webp',
324
+ format: 'webp',
325
+ });
326
+ case 'image/webp':
327
+ sourcesToGenerate.push({
328
+ mime: 'image/avif',
329
+ format: 'avif',
330
+ });
331
+ case 'image/avif':
332
+ // Nothing better
333
+ break;
319
334
  }
320
- const srcset = await generateSrcSet(htmlfile, originalImage, attrib_src, w, h, false, undefined, {
321
- toFormat: s.format,
322
- progressive: false, // avif or webp don't have progressive support
323
- });
324
- if (!srcset) {
325
- // No sourceset generated
326
- // Image is too small or can't be compressed better
327
- continue;
335
+ const sizes = img.attr('sizes');
336
+ for (const s of sourcesToGenerate) {
337
+ const sourceWithThisMimeType = picture.children(`source[type="${s.mime}"]`);
338
+ if (sourceWithThisMimeType.length > 0) {
339
+ // Ignore the creation of sources that already exist
340
+ continue;
341
+ }
342
+ const srcset = await generateSrcSet(htmlfile, originalImage, undefined, undefined, {
343
+ toFormat: s.format,
344
+ });
345
+ if (!srcset) {
346
+ // No sourceset generated
347
+ // Image is too small or can't be compressed better
348
+ continue;
349
+ }
350
+ const source = `<source ${sizes ? `sizes="${sizes}"` : ''} srcset="${srcset}" type="${s.mime}">`;
351
+ picture.prepend(source);
328
352
  }
329
- const source = `<source srcset="${srcset}" type="${s.mime}">`;
330
- picture.prepend(source);
331
- }
332
- // Reorder sources to priority order 1)avif 2)webp
333
- //
334
- function popupSource(type) {
335
- const nodes = picture.children(`source[type="${type}"]`);
336
- picture.prepend(nodes);
353
+ // Reorder sources to priority order 1)avif 2)webp
354
+ //
355
+ function popupSource(type) {
356
+ const nodes = picture.children(`source[type="${type}"]`);
357
+ picture.prepend(nodes);
358
+ }
359
+ popupSource('image/webp');
360
+ popupSource('image/avif');
337
361
  }
338
- popupSource('image/webp');
339
- popupSource('image/avif');
340
362
  }
341
363
  }
342
- async function generateSrcSet(htmlfile, originalImage, attrib_src, imageWidth, imageHeight, startWithOriginalImage, originalImageLengthToBeat, options) {
343
- const ext = path.extname(attrib_src);
344
- const fullbasename = attrib_src.slice(0, -ext.length);
364
+ async function generateSrcSet(htmlfile, originalImage, startSrc, startSrcLength, options) {
365
+ const ext = path.extname(originalImage.src);
366
+ const fullbasename = originalImage.src.slice(0, -ext.length);
345
367
  const imageSrc = (addition) => `${fullbasename}${addition}${options.toFormat === 'unchanged' ? ext : '.' + options.toFormat}`;
346
368
  // Start from original image
347
369
  let new_srcset = '';
370
+ const meta = await originalImage.getImageMeta();
371
+ const imageWidth = (meta === null || meta === void 0 ? void 0 : meta.width) || 0;
372
+ const imageHeight = (meta === null || meta === void 0 ? void 0 : meta.height) || 0;
373
+ if (!imageWidth || !imageHeight) {
374
+ // Forget about srcset
375
+ return null;
376
+ }
348
377
  // Start reduction
349
378
  const step = 300; //px
350
379
  const ratio = imageWidth / imageHeight;
351
- let valueW = !startWithOriginalImage ? imageWidth : imageWidth - step;
380
+ let valueW = !startSrc ? imageWidth : imageWidth - step;
352
381
  let valueH = Math.trunc(valueW / ratio);
353
- let previousImageSize = originalImageLengthToBeat || Number.MAX_VALUE;
382
+ let previousImageSize = startSrcLength || Number.MAX_VALUE;
354
383
  while (valueW >= config.image.srcset_min_width) {
355
384
  const src = imageSrc(`@${valueW}w`);
356
385
  const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
@@ -383,8 +412,8 @@ async function generateSrcSet(htmlfile, originalImage, attrib_src, imageWidth, i
383
412
  valueH = Math.trunc(valueW / ratio);
384
413
  }
385
414
  if (new_srcset) {
386
- return startWithOriginalImage
387
- ? `${attrib_src} ${imageWidth}w` + new_srcset
415
+ return startSrc
416
+ ? `${startSrc} ${imageWidth}w` + new_srcset
388
417
  : new_srcset.slice(2);
389
418
  }
390
419
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {