@divriots/jampack 0.7.1 → 0.8.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 CHANGED
@@ -35,12 +35,16 @@ Optimizes static websites for best user experience and best Core Web Vitals scor
35
35
 
36
36
  `jampack` is a post-processing tool that takes the output of your [Static Site Generator (aka SSG)](https://jamstack.org/generators/) and optimizes it for best user experience and best [Core Web Vitals](https://web.dev/learn-core-web-vitals/) scores.
37
37
 
38
- ## Why `jampack`?
38
+ [Read the introduction blog post](https://divriots.com/blog/introducing-jampack)
39
39
 
40
- - **Better user experience** for your website visitors. Specially on low-end devices.
41
- - **Better SEO**. Page experience now [affects ranking](https://developers.google.com/search/docs/appearance/page-experience).
42
- - **Easier maintenance**. Generating top performance webpages requires extra engineering in the composition of the pages that is counterproductive with the maintenance of the website.
43
- - **Time saving**. Spend less time optimizing and let `jampack` perform the heaving-lifting for you.
40
+ ## What can `jampack` do?
41
+
42
+ - [Optimize images](https://jampack.divriots.com/features/optimize-images)
43
+ - [Optimize assets above-the-fold](https://jampack.divriots.com/features/optimize-above-the-fold)
44
+ - [Embed small images](https://jampack.divriots.com/features/embed-small-images)
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)
44
48
 
45
49
  ## Quick use
46
50
 
@@ -51,15 +55,7 @@ npx @divriots/jampack ./dist
51
55
 
52
56
  For more options see [CLI options](https://jampack.divriots.com/cli-options).
53
57
 
54
- <div>
55
-
56
- ## What can `jampack` do?
57
-
58
- See [the list of features](https://jampack.divriots.com).
59
-
60
- </div>
61
-
62
- ## `jampack` use in the wild
58
+ ## `jampack` used in the wild
63
59
 
64
60
  - [divRIOTS.com](https://divRIOTS.com)
65
61
  - [Backlight.dev](https://backlight.dev)
@@ -70,7 +66,9 @@ See [the list of features](https://jampack.divriots.com).
70
66
  - [Components.studio](https://components.studio)
71
67
  - [keycloak.ch](https://keycloak.ch)
72
68
  - [bayjs.org](https://bayjs.org/)
73
-
69
+ - [qwind.pages.dev](https://qwind.pages.dev/)
70
+ - [Bloycey's Blog](https://bloycey.blog/)
71
+
74
72
  > Add yours here
75
73
 
76
74
  ## Why is it called `jampack`?
@@ -1,11 +1,15 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import sharp from 'sharp';
3
+ import { MimeType } from 'file-type';
4
+ export type ImageMimeType = MimeType | 'image/svg+xml';
3
5
  export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | 'avif' | undefined;
4
6
  export type Image = {
5
7
  format: ImageFormat;
6
8
  data: Buffer;
7
9
  };
8
- export declare function compressImage(data: Buffer, options: {
10
+ export type ImageOutputOptions = {
9
11
  resize?: sharp.ResizeOptions;
10
- toFormat?: 'webp' | 'pjpg' | 'avif' | 'unchanged';
11
- }): Promise<Image | undefined>;
12
+ toFormat?: 'webp' | 'avif' | 'png' | 'jpeg' | 'unchanged';
13
+ progressive?: boolean;
14
+ };
15
+ export declare function compressImage(data: Buffer, options: ImageOutputOptions): Promise<Image | undefined>;
@@ -48,6 +48,10 @@ export async function compressImage(data, options) {
48
48
  }
49
49
  return { format: 'svg', data: Buffer.from(newData.data, 'utf8') };
50
50
  }
51
+ // TODO
52
+ // Use information of input image to the destination format
53
+ // - Progressive (unless overriden)
54
+ // - Lossless
51
55
  // The bitmap images
52
56
  if (toFormat === 'unchanged') {
53
57
  switch (meta.format) {
@@ -64,8 +68,9 @@ export async function compressImage(data, options) {
64
68
  sharpFile = sharpFile.webp(createWebpOptions(config.image.webp.options_lossly) || {});
65
69
  outputFormat = 'webp';
66
70
  break;
71
+ case 'heif':
67
72
  case 'avif':
68
- sharpFile = sharpFile.avif();
73
+ sharpFile = sharpFile.avif({ effort: 4, quality: 50 }); // TODO create config for avif
69
74
  outputFormat = 'avif';
70
75
  break;
71
76
  }
@@ -73,10 +78,16 @@ export async function compressImage(data, options) {
73
78
  else {
74
79
  // To format
75
80
  switch (toFormat) {
76
- case 'pjpg':
77
- sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
81
+ case 'jpeg':
82
+ sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: options.progressive } ||
83
+ {});
78
84
  outputFormat = 'jpg';
79
85
  break;
86
+ case 'png':
87
+ sharpFile = sharpFile.png({ ...config.image.png.options, progressive: options.progressive } ||
88
+ {});
89
+ outputFormat = 'png';
90
+ break;
80
91
  case 'webp':
81
92
  sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
82
93
  ? config.image.webp.options_lossless
@@ -84,7 +95,10 @@ export async function compressImage(data, options) {
84
95
  outputFormat = 'webp';
85
96
  break;
86
97
  case 'avif':
87
- sharpFile = sharpFile.avif({ effort: 6 });
98
+ sharpFile = sharpFile.avif({
99
+ effort: 4,
100
+ quality: meta.format === 'png' ? 80 : 60, // don't use lossless avif it doesn't compress well in most png uses cases
101
+ });
88
102
  outputFormat = 'avif';
89
103
  break;
90
104
  }
package/dist/config.js CHANGED
@@ -5,7 +5,7 @@ const default_options = {
5
5
  compress: true,
6
6
  jpeg: {
7
7
  options: {
8
- quality: 80,
8
+ quality: 75,
9
9
  mozjpeg: true,
10
10
  },
11
11
  },
@@ -17,12 +17,12 @@ const default_options = {
17
17
  webp: {
18
18
  options_lossless: {
19
19
  effort: 4,
20
- quality: 80,
20
+ quality: 77,
21
21
  mode: 'lossless',
22
22
  },
23
23
  options_lossly: {
24
24
  effort: 4,
25
- quality: 80,
25
+ quality: 77,
26
26
  mode: 'lossly',
27
27
  },
28
28
  },
package/dist/optimize.js CHANGED
@@ -4,7 +4,7 @@ import * as fs from 'fs/promises';
4
4
  import cheerio from 'cheerio';
5
5
  import { isNumeric } from './utils.js';
6
6
  import config from './config.js';
7
- import { compressImage } from './compressors/images.js';
7
+ import { compressImage, } from './compressors/images.js';
8
8
  import svgToMiniDataURI from 'mini-svg-data-uri';
9
9
  import $state from './state.js';
10
10
  import kleur from 'kleur';
@@ -30,7 +30,16 @@ async function analyse(file) {
30
30
  const imgElement = imgsArray[i];
31
31
  spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')} `);
32
32
  const isAboveTheFold = imgElement.startIndex < theFold;
33
- await processImage(file, $, imgElement, isAboveTheFold);
33
+ try {
34
+ await processImage(file, $, imgElement, isAboveTheFold);
35
+ }
36
+ catch (e) {
37
+ $state.reportIssue(file, {
38
+ type: 'erro',
39
+ msg: e.message ||
40
+ `Unexpected error while processing image: ${JSON.stringify(e)}`,
41
+ });
42
+ }
34
43
  }
35
44
  // Reset spinner
36
45
  spinnerImg.text = kleur.dim(`<img> [${imgsArray.length}/${imgsArray.length}]`);
@@ -65,252 +74,320 @@ function getTheFold($) {
65
74
  if (!theFolds[0]) {
66
75
  return 0;
67
76
  }
77
+ // Pickup the position of the last `<the-fold>`
68
78
  // @ts-ignore
69
- return theFolds[0].startIndex;
79
+ return theFolds[theFolds.length - 1].startIndex;
70
80
  }
71
81
  async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
72
- try {
73
- const img = $(imgElement);
74
- /*
75
- * Attribute 'src'
76
- */
77
- const attrib_src = img.attr('src');
78
- if (!attrib_src) {
79
- $state.reportIssue(htmlfile, {
80
- type: 'warn',
81
- msg: `Missing [src] on img - processing skipped.`,
82
+ var _a;
83
+ const img = $(imgElement);
84
+ /*
85
+ * Attribute 'src'
86
+ */
87
+ const attrib_src = img.attr('src');
88
+ if (!attrib_src) {
89
+ $state.reportIssue(htmlfile, {
90
+ type: 'warn',
91
+ msg: `Missing [src] on img - processing skipped.`,
92
+ });
93
+ return;
94
+ }
95
+ /*
96
+ * Attribute 'alt'
97
+ */
98
+ const attrib_alt = img.attr('alt');
99
+ if (attrib_alt === undefined) {
100
+ $state.reportIssue(htmlfile, {
101
+ type: 'a11y',
102
+ msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
103
+ });
104
+ img.attr('alt', '');
105
+ }
106
+ if (attrib_src.startsWith('data:')) {
107
+ // Data URI image
108
+ // TODO: try to compress it
109
+ return;
110
+ }
111
+ /*
112
+ * Attribute 'loading'
113
+ */
114
+ const attr_loading = img.attr('loading');
115
+ if (isAboveTheFold) {
116
+ img.removeAttr('loading');
117
+ img.attr('fetchpriority', 'high');
118
+ }
119
+ else {
120
+ switch (attr_loading) {
121
+ case undefined:
122
+ // Go lazy by default
123
+ img.attr('loading', 'lazy');
124
+ break;
125
+ case 'eager':
126
+ img.removeAttr('loading');
127
+ break;
128
+ case 'lazy':
129
+ // Don't touch it
130
+ break;
131
+ default:
132
+ $state.reportIssue(htmlfile, {
133
+ type: 'invalid',
134
+ msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
135
+ });
136
+ }
137
+ }
138
+ /*
139
+ * Attribute 'decoding'
140
+ */
141
+ img.attr('decoding', 'async');
142
+ if (!isLocal(attrib_src)) {
143
+ // Image not local, don't touch it
144
+ return;
145
+ }
146
+ /*
147
+ * Loading image
148
+ */
149
+ const originalImage = await Resource.loadResource($state.dir, htmlfile, attrib_src);
150
+ // No file -> give up
151
+ if (!originalImage) {
152
+ $state.reportIssue(htmlfile, {
153
+ type: 'erro',
154
+ msg: `Can't find img on disk src="${attrib_src}"`,
155
+ });
156
+ return;
157
+ }
158
+ /*
159
+ * Compress image
160
+ */
161
+ let newImage;
162
+ const isInPicture = (_a = img.parent()) === null || _a === void 0 ? void 0 : _a.is('picture');
163
+ let srcToFormat = 'unchanged';
164
+ if (isInPicture || isAboveTheFold) {
165
+ // srcToFormat should not change if in picture
166
+ // If above the fold, we will create progressive images
167
+ }
168
+ else {
169
+ // Let's go to avif -> avif , webp -> webp, else * -> webp
170
+ srcToFormat =
171
+ (await originalImage.getMime()) === 'image/avif' ||
172
+ (await originalImage.getMime()) === 'image/webp'
173
+ ? 'unchanged'
174
+ : 'webp';
175
+ }
176
+ if (!$state.optimizedFiles.has(originalImage.filePathAbsolute)) {
177
+ // Let's avoid to optimize same images twice
178
+ $state.optimizedFiles.add(originalImage.filePathAbsolute);
179
+ newImage = await compressImage(await originalImage.getData(), {
180
+ toFormat: srcToFormat,
181
+ progressive: isAboveTheFold,
182
+ });
183
+ 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
185
+ ) {
186
+ // Do we need to add an new extension?
187
+ const newExtension = `.${newImage.format}`;
188
+ const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
189
+ ? ''
190
+ : newExtension;
191
+ const newFilename = originalImage.filePathAbsolute + additionalExtension;
192
+ if (!$state.args.nowrite) {
193
+ fs.writeFile(newFilename, newImage.data);
194
+ }
195
+ // Mark new file as optimized
196
+ $state.compressedFiles.add(newFilename);
197
+ // Report compression result
198
+ $state.reportSummary({
199
+ action: newFilename !== originalImage.filePathAbsolute
200
+ ? `${await originalImage.getExt()}->${newImage.format}`
201
+ : path.extname(originalImage.filePathAbsolute),
202
+ originalSize: await originalImage.getLen(),
203
+ compressedSize: newImage.data.length,
82
204
  });
83
- return;
205
+ img.attr('src', attrib_src + additionalExtension);
84
206
  }
85
- /*
86
- * Attribute 'alt'
87
- */
88
- const attrib_alt = img.attr('alt');
89
- if (attrib_alt === undefined) {
90
- $state.reportIssue(htmlfile, {
91
- type: 'a11y',
92
- msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
207
+ else {
208
+ // Report non-compression
209
+ $state.reportSummary({
210
+ action: path.extname(originalImage.filePathAbsolute),
211
+ originalSize: await originalImage.getLen(),
212
+ compressedSize: await originalImage.getLen(),
93
213
  });
94
- img.attr('alt', '');
95
214
  }
96
- if (attrib_src.startsWith('data:')) {
97
- // Data URI image
98
- // TODO: try to compress it
99
- return;
215
+ }
216
+ /*
217
+ * Embed small images
218
+ *
219
+ * TODO this is only embedding images that have
220
+ * successfully be compressed. Should embed original
221
+ * image if it fits the size.
222
+ */
223
+ let isEmbed = false;
224
+ if (newImage && newImage.data.length <= config.image.embed_size) {
225
+ let datauri = undefined;
226
+ switch (newImage.format) {
227
+ case 'svg':
228
+ datauri = svgToMiniDataURI(newImage.data.toString());
229
+ break;
230
+ case 'webp':
231
+ datauri = `data:image/webp;base64,${newImage.data.toString('base64')}`;
232
+ break;
233
+ case 'jpg':
234
+ case 'png':
235
+ // TODO but not possible in current code
236
+ break;
100
237
  }
101
- /*
102
- * Attribute 'loading'
103
- */
104
- const attr_loading = img.attr('loading');
105
- if (isAboveTheFold) {
238
+ if (datauri) {
239
+ isEmbed = true;
240
+ img.attr('src', datauri);
106
241
  img.removeAttr('loading');
107
- img.attr('fetchpriority', 'high');
108
- }
109
- else {
110
- switch (attr_loading) {
111
- case undefined:
112
- // Go lazy by default
113
- img.attr('loading', 'lazy');
114
- break;
115
- case 'eager':
116
- img.removeAttr('loading');
117
- break;
118
- case 'lazy':
119
- // Don't touch it
120
- break;
121
- default:
122
- $state.reportIssue(htmlfile, {
123
- type: 'invalid',
124
- msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
125
- });
126
- }
242
+ img.removeAttr('decoding');
243
+ $state.reportSummary({
244
+ action: `${newImage.format}->embed`,
245
+ originalSize: await originalImage.getLen(),
246
+ compressedSize: newImage.data.length,
247
+ });
127
248
  }
128
- /*
129
- * Attribute 'decoding'
130
- */
131
- img.attr('decoding', 'async');
132
- if (!isLocal(attrib_src)) {
133
- // Image not local, don't touch it
134
- return;
249
+ }
250
+ if (isEmbed) {
251
+ // Image is embed, no need for more processing
252
+ return;
253
+ }
254
+ /*
255
+ * Attribute 'width' & 'height'
256
+ */
257
+ const [w, h] = await setImageSize(htmlfile, img, originalImage);
258
+ //
259
+ // Stop here if svg
260
+ //
261
+ if ((await originalImage.getExt()) === 'svg') {
262
+ return;
263
+ }
264
+ // Image size to beat in srcsets
265
+ //
266
+ const imageLengthToBeatInSrcSet = (newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < (await originalImage.getLen())
267
+ ? newImage.data.length
268
+ : await originalImage.getLen();
269
+ /*
270
+ * Attribute 'srcset'
271
+ */
272
+ if (img.attr('srcset')) {
273
+ // If srcset are set, don't touch it.
274
+ // The compress pass will compress the images
275
+ // of the srcset
276
+ }
277
+ else {
278
+ const new_srcset = await generateSrcSet(htmlfile, originalImage, attrib_src, w, h, true, imageLengthToBeatInSrcSet, { toFormat: srcToFormat, progressive: isAboveTheFold });
279
+ if (new_srcset !== null) {
280
+ img.attr('srcset', new_srcset);
135
281
  }
136
- /*
137
- * Loading image
138
- */
139
- const originalImage = await Resource.loadResource($state.dir, htmlfile, attrib_src);
140
- // No file -> give up
141
- if (!originalImage) {
142
- $state.reportIssue(htmlfile, {
143
- type: 'erro',
144
- msg: `Can't find img on disk src="${attrib_src}"`,
145
- });
146
- return;
282
+ // Add sizes attribute if not specified
283
+ if (img.attr('srcset') && !img.attr('sizes')) {
284
+ img.attr('sizes', '100vw');
147
285
  }
148
- /*
149
- * Compress image
150
- */
151
- let newImage;
152
- const originalImageMeta = await originalImage.getImageMeta();
153
- const canBeProgressiveJpeg = isAboveTheFold && originalImageMeta && !originalImageMeta.hasAlpha;
154
- if (!$state.optimizedFiles.has(originalImage.filePathAbsolute)) {
155
- // Let's avoid to optimize same images twice
156
- $state.optimizedFiles.add(originalImage.filePathAbsolute);
157
- newImage = await compressImage(await originalImage.getData(), {
158
- toFormat: canBeProgressiveJpeg ? 'pjpg' : 'webp',
159
- });
160
- if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
161
- (newImage.data.length < (await originalImage.getLen()) ||
162
- canBeProgressiveJpeg) // Progressive jpg above the fold should get replaced even if bigger
163
- ) {
164
- // Do we need to add an new extension?
165
- const newExtension = `.${newImage.format}`;
166
- const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
167
- ? ''
168
- : newExtension;
169
- const newFilename = originalImage.filePathAbsolute + additionalExtension;
170
- if (!$state.args.nowrite) {
171
- fs.writeFile(newFilename, newImage.data);
172
- }
173
- // Mark new file as optimized
174
- $state.compressedFiles.add(newFilename);
175
- // Report compression result
176
- $state.reportSummary({
177
- action: newFilename !== originalImage.filePathAbsolute
178
- ? `${await originalImage.getExt()}->${newImage.format}`
179
- : path.extname(originalImage.filePathAbsolute),
180
- originalSize: await originalImage.getLen(),
181
- compressedSize: newImage.data.length,
286
+ }
287
+ /*
288
+ * Adding <source>'s to <picture>
289
+ */
290
+ if (isInPicture) {
291
+ 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',
182
304
  });
183
- img.attr('src', attrib_src + additionalExtension);
184
- }
185
- else {
186
- // Report non-compression
187
- $state.reportSummary({
188
- action: path.extname(originalImage.filePathAbsolute),
189
- originalSize: await originalImage.getLen(),
190
- compressedSize: await originalImage.getLen(),
305
+ case 'image/webp':
306
+ sourcesToGenerate.push({
307
+ mime: 'image/avif',
308
+ format: 'avif',
191
309
  });
192
- }
310
+ case 'image/avif':
311
+ // Nothing better
312
+ break;
193
313
  }
194
- /*
195
- * Embed small images
196
- *
197
- * TODO this is only embedding images that have
198
- * successfully be compressed. Should embed original
199
- * image if it fits the size.
200
- */
201
- let isEmbed = false;
202
- if (newImage && newImage.data.length <= config.image.embed_size) {
203
- let datauri = undefined;
204
- switch (newImage.format) {
205
- case 'svg':
206
- datauri = svgToMiniDataURI(newImage.data.toString());
207
- break;
208
- case 'webp':
209
- datauri = `data:image/webp;base64,${newImage.data.toString('base64')}`;
210
- break;
211
- case 'jpg':
212
- case 'png':
213
- // TODO but not possible in current code
214
- break;
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;
215
319
  }
216
- if (datauri) {
217
- isEmbed = true;
218
- img.attr('src', datauri);
219
- img.removeAttr('loading');
220
- img.removeAttr('decoding');
221
- $state.reportSummary({
222
- action: `${newImage.format}->embed`,
223
- originalSize: await originalImage.getLen(),
224
- compressedSize: newImage.data.length,
225
- });
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;
226
328
  }
329
+ const source = `<source srcset="${srcset}" type="${s.mime}">`;
330
+ picture.prepend(source);
227
331
  }
228
- if (isEmbed) {
229
- // Image is embed, no need for more processing
230
- return;
231
- }
232
- /*
233
- * Attribute 'width' & 'height'
234
- */
235
- const [w, h] = await setImageSize(htmlfile, img, originalImage);
332
+ // Reorder sources to priority order 1)avif 2)webp
236
333
  //
237
- // Stop here if svg
238
- //
239
- if ((await originalImage.getExt()) === 'svg') {
240
- return;
334
+ function popupSource(type) {
335
+ const nodes = picture.children(`source[type="${type}"]`);
336
+ picture.prepend(nodes);
241
337
  }
242
- /*
243
- * Attribute 'srcset'
244
- */
245
- const attr_srcset = img.attr('srcset');
246
- const attr_sizes = img.attr('sizes');
247
- if (attr_srcset || attr_sizes) {
248
- // If srcset or sizes are set, don't touch it.
249
- // The compress pass will compress the images
250
- // of the srcset
251
- //
252
- // TODO
253
- // support if sizes alone is set (no srcset) and generate the appropriate srcset
254
- }
255
- else {
256
- // Generate image set
257
- // Avif => Avif
258
- // png, jpg, ... => WebP
259
- const targetFormat = (await originalImage.getMime()) === 'image/avif' ? 'avif' : 'webp';
260
- const ext = path.extname(attrib_src);
261
- const fullbasename = attrib_src.slice(0, -ext.length);
262
- const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ? 'jpg' : targetFormat}`;
263
- // Start from original image
264
- let new_srcset = '';
265
- // Start reduction
266
- const step = 300; //px
267
- const ratio = w / h;
268
- let valueW = w - step;
269
- let valueH = Math.trunc(valueW / ratio);
270
- let previousImageSize = (newImage === null || newImage === void 0 ? void 0 : newImage.data)
271
- ? Math.min(newImage.data.length, await originalImage.getLen())
272
- : await originalImage.getLen();
273
- while (valueW > config.image.srcset_min_width) {
274
- const src = imageSrc(`@${valueW}w`);
275
- const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
276
- // Don't generate srcset file twice
277
- if (!$state.compressedFiles.has(absoluteFilename)) {
278
- // Add file to list avoid recompression
279
- $state.compressedFiles.add(absoluteFilename);
280
- const compressedImage = await compressImage(await originalImage.getData(), {
281
- resize: { width: valueW, height: valueH },
282
- toFormat: canBeProgressiveJpeg ? 'pjpg' : targetFormat,
283
- });
284
- if (!(compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) ||
285
- compressedImage.data.length >= previousImageSize) {
286
- // jampack was not able to compress below the original image
287
- // Let's move on
288
- continue;
289
- }
290
- if (!$state.args.nowrite) {
291
- fs.writeFile(absoluteFilename, compressedImage.data);
292
- }
293
- // Add previous smaller file size
294
- previousImageSize = compressedImage.data.length;
295
- }
296
- new_srcset += `, ${src} ${valueW}w`;
297
- // reduce size
298
- valueW -= step;
299
- valueH = Math.trunc(valueW / ratio);
338
+ popupSource('image/webp');
339
+ popupSource('image/avif');
340
+ }
341
+ }
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);
345
+ const imageSrc = (addition) => `${fullbasename}${addition}${options.toFormat === 'unchanged' ? ext : '.' + options.toFormat}`;
346
+ // Start from original image
347
+ let new_srcset = '';
348
+ // Start reduction
349
+ const step = 300; //px
350
+ const ratio = imageWidth / imageHeight;
351
+ let valueW = !startWithOriginalImage ? imageWidth : imageWidth - step;
352
+ let valueH = Math.trunc(valueW / ratio);
353
+ let previousImageSize = originalImageLengthToBeat || Number.MAX_VALUE;
354
+ while (valueW >= config.image.srcset_min_width) {
355
+ const src = imageSrc(`@${valueW}w`);
356
+ const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
357
+ let doAddToSrcSet = true;
358
+ // Don't generate srcset file twice
359
+ if (!$state.compressedFiles.has(absoluteFilename)) {
360
+ const compressedImage = await compressImage(await originalImage.getData(), { ...options, resize: { width: valueW, height: valueH } });
361
+ if (!(compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) ||
362
+ compressedImage.data.length >= previousImageSize) {
363
+ // New image is not compressed or bigger than previous image in srcset
364
+ // Don't add to srcset
365
+ doAddToSrcSet = false;
300
366
  }
301
- if (new_srcset) {
302
- img.attr('srcset', `${img.attr('src')} ${w}w` + new_srcset);
303
- img.attr('sizes', '100vw');
367
+ else {
368
+ // Write file
369
+ if (!$state.args.nowrite) {
370
+ fs.writeFile(absoluteFilename, compressedImage.data);
371
+ }
372
+ // Set new previous size to beat
373
+ previousImageSize = compressedImage.data.length;
374
+ // Add file to list avoid recompression
375
+ $state.compressedFiles.add(absoluteFilename);
304
376
  }
305
377
  }
378
+ if (doAddToSrcSet) {
379
+ new_srcset += `, ${src} ${valueW}w`;
380
+ }
381
+ // reduce size
382
+ valueW -= step;
383
+ valueH = Math.trunc(valueW / ratio);
306
384
  }
307
- catch (e) {
308
- $state.reportIssue(htmlfile, {
309
- type: 'erro',
310
- msg: e.message ||
311
- `Unexpected error while processing image: ${JSON.stringify(e)}`,
312
- });
385
+ if (new_srcset) {
386
+ return startWithOriginalImage
387
+ ? `${attrib_src} ${imageWidth}w` + new_srcset
388
+ : new_srcset.slice(2);
313
389
  }
390
+ return null;
314
391
  }
315
392
  async function setImageSize(htmlfile, img, image) {
316
393
  let width = img.attr('width');
@@ -321,7 +398,7 @@ async function setImageSize(htmlfile, img, image) {
321
398
  if (width !== undefined) {
322
399
  if (!isNumeric(width)) {
323
400
  $state.reportIssue(htmlfile, {
324
- type: 'warn',
401
+ type: 'fix',
325
402
  msg: `Invalid width attribute format: "${width}" - overriding for ${image.src}`,
326
403
  });
327
404
  width = undefined;
@@ -330,7 +407,7 @@ async function setImageSize(htmlfile, img, image) {
330
407
  if (height !== undefined) {
331
408
  if (!isNumeric(height)) {
332
409
  $state.reportIssue(htmlfile, {
333
- type: 'warn',
410
+ type: 'fix',
334
411
  msg: `Invalid height attribute format: "${height}" - overriding for ${image.src}`,
335
412
  });
336
413
  height = undefined;
package/dist/state.d.ts CHANGED
@@ -14,7 +14,7 @@ type Summary = {
14
14
  dataLenCompressed: number;
15
15
  };
16
16
  type Issue = {
17
- type: 'invalid' | 'a11y' | 'perf' | 'erro' | 'warn';
17
+ type: 'invalid' | 'a11y' | 'perf' | 'erro' | 'fix' | 'warn';
18
18
  msg: string;
19
19
  };
20
20
  export declare class GlobalState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {
@@ -16,27 +16,27 @@
16
16
  "/dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@swc/core": "^1.3.21",
19
+ "@swc/core": "^1.3.30",
20
20
  "@types/cheerio": "^0.22.31",
21
21
  "@types/commander": "^2.12.2",
22
22
  "@types/csso": "^5.0.0",
23
23
  "@types/html-minifier-terser": "^7.0.0",
24
24
  "@types/ora": "^3.2.0",
25
- "@types/sharp": "^0.31.0",
25
+ "@types/sharp": "^0.31.1",
26
26
  "@types/svgo": "^2.6.4",
27
27
  "@types/table": "^6.3.2",
28
28
  "cheerio": "0.22.0",
29
29
  "commander": "^9.4.1",
30
30
  "csso": "^5.0.5",
31
31
  "file-type": "^18.0.0",
32
- "globby": "^13.1.2",
32
+ "globby": "^13.1.3",
33
33
  "hasha": "^5.2.2",
34
34
  "html-minifier-terser": "^7.1.0",
35
35
  "kleur": "^4.1.5",
36
36
  "lightningcss": "^1.18.0",
37
37
  "mini-svg-data-uri": "^1.4.4",
38
38
  "ora": "^6.1.2",
39
- "sharp": "^0.31.2",
39
+ "sharp": "^0.31.3",
40
40
  "svgo": "^2.8.0",
41
41
  "table": "^6.8.1"
42
42
  },
@@ -45,7 +45,7 @@
45
45
  "shx": "^0.3.3",
46
46
  "ts-node": "^10.2.1",
47
47
  "tslib": "^2.4.1",
48
- "typescript": "^4.9.3"
48
+ "typescript": "^4.9.4"
49
49
  },
50
50
  "scripts": {
51
51
  "watch": "pnpm build --watch",