UNPKG

28.5 kBJavaScriptView Raw
1"use strict";
2
3const path = require("path");
4/** @typedef {import("./index").WorkerResult} WorkerResult */
5
6/** @typedef {import("./index").SquooshOptions} SquooshOptions */
7
8/** @typedef {import("imagemin").Options} ImageminOptions */
9
10/** @typedef {import("webpack").WebpackError} WebpackError */
11
12
13const notSettled = Symbol("not-settled");
14/**
15 * @template T
16 * @typedef {() => Promise<T>} Task
17 */
18
19/**
20 * Run tasks with limited concurency.
21 * @template T
22 * @param {number} limit - Limit of tasks that run at once.
23 * @param {Task<T>[]} tasks - List of tasks to run.
24 * @returns {Promise<T[]>} A promise that fulfills to an array of the results
25 */
26
27function throttleAll(limit, tasks) {
28 if (!Number.isInteger(limit) || limit < 1) {
29 throw new TypeError(`Expected 'limit' to be a finite number > 0, got \`${limit}\` (${typeof limit})`);
30 }
31
32 if (!Array.isArray(tasks) || !tasks.every(task => typeof task === "function")) {
33 throw new TypeError("Expected 'tasks' to be a list of functions returning a promise");
34 }
35
36 return new Promise((resolve, reject) => {
37 // eslint-disable-next-line unicorn/new-for-builtins
38 const result = Array(tasks.length).fill(notSettled);
39 const entries = tasks.entries();
40
41 const next = () => {
42 const {
43 done,
44 value
45 } = entries.next();
46
47 if (done) {
48 const isLast = !result.includes(notSettled);
49
50 if (isLast) {
51 // eslint-disable-next-line node/callback-return
52 resolve(result);
53 }
54
55 return;
56 }
57
58 const [index, task] = value;
59 /**
60 * @param {T} i
61 */
62
63 const onFulfilled = i => {
64 result[index] = i;
65 next();
66 };
67
68 task().then(onFulfilled, reject);
69 }; // eslint-disable-next-line unicorn/new-for-builtins
70
71
72 Array(limit).fill(0).forEach(next);
73 });
74}
75
76const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/;
77const WINDOWS_PATH_REGEX = /^[a-zA-Z]:\\/;
78/**
79 * @param {string} url
80 * @returns {boolean}
81 */
82
83function isAbsoluteURL(url) {
84 if (WINDOWS_PATH_REGEX.test(url)) {
85 return false;
86 }
87
88 return ABSOLUTE_URL_REGEX.test(url);
89}
90/**
91 * @callback Uint8ArrayUtf8ByteString
92 * @param {number[] | Uint8Array} array
93 * @param {number} start
94 * @param {number} end
95 * @returns {string}
96 */
97
98/** @type {Uint8ArrayUtf8ByteString} */
99
100
101const uint8ArrayUtf8ByteString = (array, start, end) => String.fromCodePoint(...array.slice(start, end));
102/**
103 * @callback StringToBytes
104 * @param {string} string
105 * @returns {number[]}
106 */
107
108/** @type {StringToBytes} */
109
110
111const stringToBytes = string => // eslint-disable-next-line unicorn/prefer-code-point
112[...string].map(character => character.charCodeAt(0));
113/**
114 * @param {ArrayBuffer | ArrayLike<number>} input
115 * @returns {{ext: string, mime: string} | undefined}
116 */
117
118
119function fileTypeFromBuffer(input) {
120 if (!(input instanceof Uint8Array || input instanceof ArrayBuffer || Buffer.isBuffer(input))) {
121 throw new TypeError(`Expected the \`input\` argument to be of type \`Uint8Array\` or \`Buffer\` or \`ArrayBuffer\`, got \`${typeof input}\``);
122 }
123
124 const buffer = input instanceof Uint8Array ? input : new Uint8Array(input);
125
126 if (!(buffer && buffer.length > 1)) {
127 return;
128 }
129 /**
130 * @param {number[]} header
131 * @param {{offset: number, mask?: number[]}} [options]
132 * @returns {boolean}
133 */
134
135
136 const check = (header, options) => {
137 // eslint-disable-next-line no-param-reassign
138 options = {
139 offset: 0,
140 ...options
141 };
142
143 for (let i = 0; i < header.length; i++) {
144 if (options.mask) {
145 // eslint-disable-next-line no-bitwise
146 if (header[i] !== (options.mask[i] & buffer[i + options.offset])) {
147 return false;
148 }
149 } else if (header[i] !== buffer[i + options.offset]) {
150 return false;
151 }
152 }
153
154 return true;
155 };
156 /**
157 * @param {string} header
158 * @param {{offset: number, mask?: number[]}} [options]
159 * @returns {boolean}
160 */
161
162
163 const checkString = (header, options) => check(stringToBytes(header), options);
164
165 if (check([0xff, 0xd8, 0xff])) {
166 return {
167 ext: "jpg",
168 mime: "image/jpeg"
169 };
170 }
171
172 if (check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
173 // APNG format (https://wiki.mozilla.org/APNG_Specification)
174 // 1. Find the first IDAT (image data) chunk (49 44 41 54)
175 // 2. Check if there is an "acTL" chunk before the IDAT one (61 63 54 4C)
176 // Offset calculated as follows:
177 // - 8 bytes: PNG signature
178 // - 4 (length) + 4 (chunk type) + 13 (chunk data) + 4 (CRC): IHDR chunk
179 const startIndex = 33;
180 const firstImageDataChunkIndex = buffer.findIndex((el, i) => i >= startIndex && buffer[i] === 0x49 && buffer[i + 1] === 0x44 && buffer[i + 2] === 0x41 && buffer[i + 3] === 0x54);
181 const sliced = buffer.subarray(startIndex, firstImageDataChunkIndex);
182
183 if (sliced.findIndex((el, i) => sliced[i] === 0x61 && sliced[i + 1] === 0x63 && sliced[i + 2] === 0x54 && sliced[i + 3] === 0x4c) >= 0) {
184 return {
185 ext: "apng",
186 mime: "image/apng"
187 };
188 }
189
190 return {
191 ext: "png",
192 mime: "image/png"
193 };
194 }
195
196 if (check([0x47, 0x49, 0x46])) {
197 return {
198 ext: "gif",
199 mime: "image/gif"
200 };
201 }
202
203 if (check([0x57, 0x45, 0x42, 0x50], {
204 offset: 8
205 })) {
206 return {
207 ext: "webp",
208 mime: "image/webp"
209 };
210 }
211
212 if (check([0x46, 0x4c, 0x49, 0x46])) {
213 return {
214 ext: "flif",
215 mime: "image/flif"
216 };
217 } // `cr2`, `orf`, and `arw` need to be before `tif` check
218
219
220 if ((check([0x49, 0x49, 0x2a, 0x0]) || check([0x4d, 0x4d, 0x0, 0x2a])) && check([0x43, 0x52], {
221 offset: 8
222 })) {
223 return {
224 ext: "cr2",
225 mime: "image/x-canon-cr2"
226 };
227 }
228
229 if (check([0x49, 0x49, 0x52, 0x4f, 0x08, 0x00, 0x00, 0x00, 0x18])) {
230 return {
231 ext: "orf",
232 mime: "image/x-olympus-orf"
233 };
234 }
235
236 if (check([0x49, 0x49, 0x2a, 0x00]) && (check([0x10, 0xfb, 0x86, 0x01], {
237 offset: 4
238 }) || check([0x08, 0x00, 0x00, 0x00], {
239 offset: 4
240 })) && // This pattern differentiates ARW from other TIFF-ish file types:
241 check([0x00, 0xfe, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x01], {
242 offset: 9
243 })) {
244 return {
245 ext: "arw",
246 mime: "image/x-sony-arw"
247 };
248 }
249
250 if (check([0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00]) && (check([0x2d, 0x00, 0xfe, 0x00], {
251 offset: 8
252 }) || check([0x27, 0x00, 0xfe, 0x00], {
253 offset: 8
254 }))) {
255 return {
256 ext: "dng",
257 mime: "image/x-adobe-dng"
258 };
259 }
260
261 if (check([0x49, 0x49, 0x2a, 0x00]) && check([0x1c, 0x00, 0xfe, 0x00], {
262 offset: 8
263 })) {
264 return {
265 ext: "nef",
266 mime: "image/x-nikon-nef"
267 };
268 }
269
270 if (check([0x49, 0x49, 0x55, 0x00, 0x18, 0x00, 0x00, 0x00, 0x88, 0xe7, 0x74, 0xd8])) {
271 return {
272 ext: "rw2",
273 mime: "image/x-panasonic-rw2"
274 };
275 } // `raf` is here just to keep all the raw image detectors together.
276
277
278 if (checkString("FUJIFILMCCD-RAW")) {
279 return {
280 ext: "raf",
281 mime: "image/x-fujifilm-raf"
282 };
283 }
284
285 if (check([0x49, 0x49, 0x2a, 0x0]) || check([0x4d, 0x4d, 0x0, 0x2a])) {
286 return {
287 ext: "tif",
288 mime: "image/tiff"
289 };
290 }
291
292 if (check([0x42, 0x4d])) {
293 return {
294 ext: "bmp",
295 mime: "image/bmp"
296 };
297 }
298
299 if (check([0x49, 0x49, 0xbc])) {
300 return {
301 ext: "jxr",
302 mime: "image/vnd.ms-photo"
303 };
304 }
305
306 if (check([0x38, 0x42, 0x50, 0x53])) {
307 return {
308 ext: "psd",
309 mime: "image/vnd.adobe.photoshop"
310 };
311 }
312
313 if (checkString("ftyp", {
314 offset: 4
315 }) && // eslint-disable-next-line no-bitwise
316 (buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
317 ) {
318 // They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
319 // For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
320 const brandMajor = uint8ArrayUtf8ByteString(buffer, 8, 12).replace("\0", " ").trim(); // eslint-disable-next-line default-case
321
322 switch (brandMajor) {
323 case "avif":
324 return {
325 ext: "avif",
326 mime: "image/avif"
327 };
328
329 case "mif1":
330 return {
331 ext: "heic",
332 mime: "image/heif"
333 };
334
335 case "msf1":
336 return {
337 ext: "heic",
338 mime: "image/heif-sequence"
339 };
340
341 case "heic":
342 case "heix":
343 return {
344 ext: "heic",
345 mime: "image/heic"
346 };
347
348 case "hevc":
349 case "hevx":
350 return {
351 ext: "heic",
352 mime: "image/heic-sequence"
353 };
354 }
355 }
356
357 if (check([0x00, 0x00, 0x01, 0x00])) {
358 return {
359 ext: "ico",
360 mime: "image/x-icon"
361 };
362 }
363
364 if (check([0x00, 0x00, 0x02, 0x00])) {
365 return {
366 ext: "cur",
367 mime: "image/x-icon"
368 };
369 }
370
371 if (check([0x42, 0x50, 0x47, 0xfb])) {
372 return {
373 ext: "bpg",
374 mime: "image/bpg"
375 };
376 }
377
378 if (check([0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a])) {
379 // JPEG-2000 family
380 if (check([0x6a, 0x70, 0x32, 0x20], {
381 offset: 20
382 })) {
383 return {
384 ext: "jp2",
385 mime: "image/jp2"
386 };
387 }
388
389 if (check([0x6a, 0x70, 0x78, 0x20], {
390 offset: 20
391 })) {
392 return {
393 ext: "jpx",
394 mime: "image/jpx"
395 };
396 }
397
398 if (check([0x6a, 0x70, 0x6d, 0x20], {
399 offset: 20
400 })) {
401 return {
402 ext: "jpm",
403 mime: "image/jpm"
404 };
405 }
406
407 if (check([0x6d, 0x6a, 0x70, 0x32], {
408 offset: 20
409 })) {
410 return {
411 ext: "mj2",
412 mime: "image/mj2"
413 };
414 }
415 }
416
417 if (check([0xff, 0x0a]) || check([0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a])) {
418 return {
419 ext: "jxl",
420 mime: "image/jxl"
421 };
422 }
423
424 if (check([0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a])) {
425 return {
426 ext: "ktx",
427 mime: "image/ktx"
428 };
429 }
430}
431/**
432 * @typedef {Object} MetaData
433 * @property {Array<Error>} warnings
434 * @property {Array<Error>} errors
435 */
436
437
438class InvalidConfigError extends Error {
439 /**
440 * @param {string | undefined} message
441 */
442 constructor(message) {
443 super(message);
444 this.name = "InvalidConfigError";
445 }
446
447}
448/**
449 * @template T
450 * @param {ImageminOptions} imageminConfig
451 * @returns {Promise<ImageminOptions>}
452 */
453
454
455async function imageminNormalizeConfig(imageminConfig) {
456 if (!imageminConfig || !imageminConfig.plugins || imageminConfig.plugins && imageminConfig.plugins.length === 0) {
457 throw new Error("No plugins found for `imagemin`, please read documentation");
458 }
459 /**
460 * @type {import("imagemin").Plugin[]}
461 */
462
463
464 const plugins = [];
465
466 for (const plugin of imageminConfig.plugins) {
467 const isPluginArray = Array.isArray(plugin);
468
469 if (typeof plugin === "string" || isPluginArray) {
470 const pluginName = isPluginArray ? plugin[0] : plugin;
471 const pluginOptions = isPluginArray ? plugin[1] : undefined;
472 let requiredPlugin = null;
473 let requiredPluginName = `imagemin-${pluginName}`;
474
475 try {
476 // @ts-ignore
477 // eslint-disable-next-line no-await-in-loop
478 requiredPlugin = (await import(requiredPluginName)).default(pluginOptions);
479 } catch {
480 requiredPluginName = pluginName;
481
482 try {
483 // @ts-ignore
484 // eslint-disable-next-line no-await-in-loop
485 requiredPlugin = (await import(requiredPluginName)).default(pluginOptions);
486 } catch {
487 const pluginNameForError = pluginName.startsWith("imagemin") ? pluginName : `imagemin-${pluginName}`;
488 throw new Error(`Unknown plugin: ${pluginNameForError}\n\nDid you forget to install the plugin?\nYou can install it with:\n\n$ npm install ${pluginNameForError} --save-dev\n$ yarn add ${pluginNameForError} --dev`);
489 } // Nothing
490
491 } // let version = "unknown";
492 // try {
493 // // eslint-disable-next-line import/no-dynamic-require
494 // ({ version } = require(`${requiredPluginName}/package.json`));
495 // } catch {
496 // // Nothing
497 // }
498 // /** @type {Array<Object>} imageminConfig.pluginsMeta */
499 // pluginsMeta.push([
500 // {
501 // name: requiredPluginName,
502 // options: pluginOptions || {},
503 // version,
504 // },
505 // ]);
506
507
508 plugins.push(requiredPlugin);
509 } else {
510 throw new InvalidConfigError(`Invalid plugin configuration '${JSON.stringify(plugin)}', plugin configuration should be 'string' or '[string, object]'"`);
511 }
512 }
513
514 return {
515 plugins
516 };
517}
518/**
519 * @template T
520 * @param {WorkerResult} original
521 * @param {T} minimizerOptions
522 * @returns {Promise<WorkerResult>}
523 */
524
525
526async function imageminGenerate(original, minimizerOptions) {
527 const minimizerOptionsNormalized =
528 /** @type {ImageminOptions} */
529 await imageminNormalizeConfig(
530 /** @type {ImageminOptions} */
531
532 /** @type {?} */
533 minimizerOptions || {}); // @ts-ignore
534 // eslint-disable-next-line node/no-unpublished-import
535
536 const imagemin = (await import("imagemin")).default;
537 let result;
538
539 try {
540 // @ts-ignore
541 result = await imagemin.buffer(original.data, minimizerOptionsNormalized);
542 } catch (error) {
543 const originalError = error instanceof Error ? error : new Error(
544 /** @type {string} */
545 error);
546 const newError = new Error(`Error with '${original.filename}': ${originalError.message}`);
547 original.info.original = true;
548 original.errors.push(newError);
549 return original;
550 }
551
552 const {
553 ext: extOutput
554 } = fileTypeFromBuffer(result) || {};
555 const extInput = path.extname(original.filename).slice(1).toLowerCase();
556 let newFilename = original.filename;
557
558 if (extOutput && extInput !== extOutput) {
559 newFilename = original.filename.replace(new RegExp(`${extInput}$`), `${extOutput}`);
560 }
561
562 return {
563 filename: newFilename,
564 data: result,
565 warnings: [...original.warnings],
566 errors: [...original.errors],
567 info: { ...original.info,
568 generated: true,
569 generatedBy: original.info && original.info.generatedBy ? ["imagemin", ...original.info.generatedBy] : ["imagemin"]
570 }
571 };
572}
573/**
574 * @template T
575 * @param {WorkerResult} original
576 * @param {T} options
577 * @returns {Promise<WorkerResult>}
578 */
579
580
581async function imageminMinify(original, options) {
582 const minimizerOptionsNormalized =
583 /** @type {ImageminOptions} */
584 await imageminNormalizeConfig(
585 /** @type {ImageminOptions} */
586
587 /** @type {?} */
588 options || {}); // @ts-ignore
589 // eslint-disable-next-line node/no-unpublished-import
590
591 const imagemin = (await import("imagemin")).default;
592 let result;
593
594 try {
595 // @ts-ignore
596 result = await imagemin.buffer(original.data, minimizerOptionsNormalized);
597 } catch (error) {
598 const originalError = error instanceof Error ? error : new Error(
599 /** @type {string} */
600 error);
601 const newError = new Error(`Error with '${original.filename}': ${originalError.message}`);
602 original.info.original = true;
603 original.errors.push(newError);
604 return original;
605 }
606
607 if (!isAbsoluteURL(original.filename)) {
608 const extInput = path.extname(original.filename).slice(1).toLowerCase();
609 const {
610 ext: extOutput
611 } = fileTypeFromBuffer(result) || {};
612
613 if (extOutput && extInput !== extOutput) {
614 original.info.original = true;
615 original.warnings.push(new Error(`"imageminMinify" function do not support generate to "${extOutput}" from "${original.filename}". Please use "imageminGenerate" function.`));
616 return original;
617 }
618 }
619
620 return {
621 filename: original.filename,
622 data: result,
623 warnings: [...original.warnings],
624 errors: [...original.errors],
625 info: { ...original.info,
626 minimized: true,
627 minimizedBy: original.info && original.info.minimizedBy ? ["imagemin", ...original.info.minimizedBy] : ["imagemin"]
628 }
629 };
630}
631/**
632 * @type {any}
633 */
634
635
636let pool;
637/**
638 * @param {number} threads
639 * @returns {any}
640 */
641
642function squooshImagePoolCreate(threads = 1) {
643 // eslint-disable-next-line node/no-unpublished-require
644 const {
645 ImagePool
646 } = require("@squoosh/lib"); // TODO https://github.com/GoogleChromeLabs/squoosh/issues/1111,
647 // TODO https://github.com/GoogleChromeLabs/squoosh/issues/1012
648 //
649 // Due to the above errors, we use the value "1", it is faster and consumes less memory in common use.
650 //
651 // Also we don't know how many image (modules are built asynchronously) we will have so we can't setup
652 // the correct value and creating child processes takes a long time, unfortunately there is no perfect solution here,
653 // maybe we should provide an option for this (or API for warm up), so if you are reading this feel free to open the issue
654
655
656 return new ImagePool(threads);
657}
658
659function squooshImagePoolSetup() {
660 if (!pool) {
661 const os = require("os"); // In some cases cpus() returns undefined
662 // https://github.com/nodejs/node/issues/19022
663
664
665 const cpus = os.cpus() || {
666 length: 1
667 };
668 pool = squooshImagePoolCreate(Math.max(cpus.length - 1, 1)); // workarounds for https://github.com/GoogleChromeLabs/squoosh/issues/1152
669 // @ts-ignore
670
671 delete globalThis.navigator;
672 }
673}
674
675async function squooshImagePoolTeardown() {
676 if (pool) {
677 await pool.close(); // eslint-disable-next-line require-atomic-updates
678
679 pool = undefined;
680 }
681}
682/**
683 * @template T
684 * @param {WorkerResult} original
685 * @param {T} minifyOptions
686 * @returns {Promise<WorkerResult>}
687 */
688
689
690async function squooshGenerate(original, minifyOptions) {
691 // eslint-disable-next-line node/no-unpublished-require
692 const squoosh = require("@squoosh/lib");
693
694 const isReusePool = Boolean(pool);
695 const imagePool = pool || squooshImagePoolCreate();
696 const image = imagePool.ingestImage(new Uint8Array(original.data));
697 const squooshOptions =
698 /** @type {SquooshOptions} */
699 minifyOptions || {};
700 /**
701 * @type {undefined | Object.<string, any>}
702 */
703
704 let preprocessors;
705
706 for (const preprocessor of Object.keys(squoosh.preprocessors)) {
707 if (typeof squooshOptions[preprocessor] !== "undefined") {
708 if (!preprocessors) {
709 preprocessors = {};
710 }
711
712 preprocessors[preprocessor] = squooshOptions[preprocessor];
713 }
714 }
715
716 if (typeof preprocessors !== "undefined") {
717 await image.preprocess(preprocessors);
718 }
719
720 const {
721 encodeOptions
722 } = squooshOptions;
723
724 try {
725 await image.encode(encodeOptions);
726 } catch (error) {
727 if (!isReusePool) {
728 await imagePool.close();
729 }
730
731 const originalError = error instanceof Error ? error : new Error(
732 /** @type {string} */
733 error);
734 const newError = new Error(`Error with '${original.filename}': ${originalError.message}`);
735 original.info.original = true;
736 original.errors.push(newError);
737 return original;
738 }
739
740 if (!isReusePool) {
741 await imagePool.close();
742 }
743
744 if (Object.keys(image.encodedWith).length === 0) {
745 // eslint-disable-next-line require-atomic-updates
746 original.info.original = true;
747 original.errors.push(new Error(`No result from 'squoosh' for '${original.filename}', please configure the 'encodeOptions' option to generate images`));
748 return original;
749 }
750
751 if (Object.keys(image.encodedWith).length > 1) {
752 // eslint-disable-next-line require-atomic-updates
753 original.info.original = true;
754 original.errors.push(new Error(`Multiple values for the 'encodeOptions' option is not supported for '${original.filename}', specify only one codec for the generator`));
755 return original;
756 }
757
758 const ext = path.extname(original.filename).toLowerCase();
759 const {
760 binary,
761 extension
762 } = await Object.values(image.encodedWith)[0];
763 const newFilename = original.filename.replace(new RegExp(`${ext}$`), `.${extension}`);
764 return {
765 filename: newFilename,
766 data: Buffer.from(binary),
767 warnings: [...original.warnings],
768 errors: [...original.errors],
769 info: { ...original.info,
770 generated: true,
771 generatedBy: original.info && original.info.generatedBy ? ["squoosh", ...original.info.generatedBy] : ["squoosh"]
772 }
773 };
774}
775
776squooshGenerate.setup = squooshImagePoolSetup;
777squooshGenerate.teardown = squooshImagePoolTeardown;
778/**
779 * @template T
780 * @param {WorkerResult} original
781 * @param {T} options
782 * @returns {Promise<WorkerResult>}
783 */
784
785async function squooshMinify(original, options) {
786 // eslint-disable-next-line node/no-unpublished-require
787 const squoosh = require("@squoosh/lib");
788
789 const {
790 encoders
791 } = squoosh;
792 /**
793 * @type {Record<string, string>}
794 */
795
796 const targets = {};
797
798 for (const [codec, {
799 extension
800 }] of Object.entries(encoders)) {
801 const extensionNormalized = extension.toLowerCase();
802
803 if (extensionNormalized === "jpg") {
804 targets.jpeg = codec;
805 }
806
807 targets[extensionNormalized] = codec;
808 }
809
810 const ext = path.extname(original.filename).slice(1).toLowerCase();
811 const targetCodec = targets[ext];
812
813 if (!targetCodec) {
814 original.info.original = true;
815 return original;
816 }
817
818 const isReusePool = Boolean(pool);
819 const imagePool = pool || squooshImagePoolCreate();
820 const image = imagePool.ingestImage(new Uint8Array(original.data));
821 const squooshOptions =
822 /** @type {SquooshOptions} */
823 options || {};
824 /**
825 * @type {undefined | Object.<string, any>}
826 */
827
828 let preprocessors;
829
830 for (const preprocessor of Object.keys(squoosh.preprocessors)) {
831 if (typeof squooshOptions[preprocessor] !== "undefined") {
832 if (!preprocessors) {
833 preprocessors = {};
834 }
835
836 preprocessors[preprocessor] = squooshOptions[preprocessor];
837 }
838 }
839
840 if (typeof preprocessors !== "undefined") {
841 await image.preprocess(preprocessors);
842 }
843
844 const {
845 encodeOptions = {}
846 } = squooshOptions;
847
848 if (!encodeOptions[targetCodec]) {
849 encodeOptions[targetCodec] = {};
850 }
851
852 try {
853 await image.encode({
854 [targetCodec]: encodeOptions[targetCodec]
855 });
856 } catch (error) {
857 if (!isReusePool) {
858 await imagePool.close();
859 }
860
861 const originalError = error instanceof Error ? error : new Error(
862 /** @type {string} */
863 error);
864 const newError = new Error(`Error with '${original.filename}': ${originalError.message}`);
865 original.info.original = true;
866 original.errors.push(newError);
867 return original;
868 }
869
870 if (!isReusePool) {
871 await imagePool.close();
872 }
873
874 const {
875 binary
876 } = await image.encodedWith[targets[ext]];
877 return {
878 filename: original.filename,
879 data: Buffer.from(binary),
880 warnings: [...original.warnings],
881 errors: [...original.errors],
882 info: { ...original.info,
883 minimized: true,
884 minimizedBy: original.info && original.info.minimizedBy ? ["squoosh", ...original.info.minimizedBy] : ["squoosh"]
885 }
886 };
887}
888
889squooshMinify.setup = squooshImagePoolSetup;
890squooshMinify.teardown = squooshImagePoolTeardown;
891/** @typedef {import("sharp")} SharpLib */
892
893/** @typedef {import("sharp").Sharp} Sharp */
894
895/** @typedef {import("sharp").ResizeOptions & { enabled?: boolean }} ResizeOptions */
896
897/**
898 * @typedef SharpEncodeOptions
899 * @type {object}
900 * @property {import("sharp").AvifOptions} [avif]
901 * @property {import("sharp").GifOptions} [gif]
902 * @property {import("sharp").HeifOptions} [heif]
903 * @property {import("sharp").JpegOptions} [jpeg]
904 * @property {import("sharp").JpegOptions} [jpg]
905 * @property {import("sharp").PngOptions} [png]
906 * @property {import("sharp").WebpOptions} [webp]
907 */
908
909/**
910 * @typedef SharpFormat
911 * @type {keyof SharpEncodeOptions}
912 */
913
914/**
915 * @typedef SharpOptions
916 * @type {object}
917 * @property {ResizeOptions} [resize]
918 * @property {number | 'auto'} [rotate]
919 * @property {SizeSuffix} [sizeSuffix]
920 * @property {SharpEncodeOptions} [encodeOptions]
921 */
922
923/**
924 * @typedef SizeSuffix
925 * @type {(width: number, height: number) => string}
926 */
927// https://github.com/lovell/sharp/blob/e40a881ab4a5e7b0e37ba17e31b3b186aef8cbf6/lib/output.js#L7-L23
928
929const SHARP_FORMATS = new Map([["avif", "avif"], ["gif", "gif"], ["heic", "heif"], ["heif", "heif"], ["j2c", "jp2"], ["j2k", "jp2"], ["jp2", "jp2"], ["jpeg", "jpeg"], ["jpg", "jpeg"], ["jpx", "jp2"], ["png", "png"], ["raw", "raw"], ["tif", "tiff"], ["tiff", "tiff"], ["webp", "webp"]]);
930/**
931 * @param {WorkerResult} original
932 * @param {0 | 1} action
933 * @param {SharpOptions} minimizerOptions
934 * @param {SharpFormat | null} targetFormat
935 * @returns {Promise<WorkerResult>}
936 */
937
938async function sharpTransform(original, action, minimizerOptions = {}, targetFormat = null) {
939 var _minimizerOptions$enc;
940
941 const inputExt = path.extname(original.filename).slice(1).toLowerCase();
942
943 if (!SHARP_FORMATS.has(inputExt)) {
944 original.info.original = true;
945 return original;
946 }
947 /** @type {SharpLib} */
948 // eslint-disable-next-line node/no-unpublished-require
949
950
951 const sharp = require("sharp");
952
953 const imagePipeline = sharp(original.data); // ====== rotate ======
954
955 if (typeof minimizerOptions.rotate === "number") {
956 imagePipeline.rotate(minimizerOptions.rotate);
957 } else if (minimizerOptions.rotate === "auto") {
958 imagePipeline.rotate();
959 } // ====== resize ======
960
961
962 if (minimizerOptions.resize && (minimizerOptions.resize.width || minimizerOptions.resize.height) && minimizerOptions.resize.enabled !== false) {
963 imagePipeline.resize(minimizerOptions.resize.width, minimizerOptions.resize.height);
964 } // ====== convert ======
965
966
967 const imageMetadata = await imagePipeline.metadata();
968 const outputFormat = targetFormat !== null && targetFormat !== void 0 ? targetFormat :
969 /** @type {SharpFormat} */
970 imageMetadata.format;
971 const encodeOptions = (_minimizerOptions$enc = minimizerOptions.encodeOptions) === null || _minimizerOptions$enc === void 0 ? void 0 : _minimizerOptions$enc[outputFormat];
972 imagePipeline.toFormat(outputFormat, encodeOptions);
973 const result = await imagePipeline.toBuffer({
974 resolveWithObject: true
975 }); // ====== rename ======
976
977 const outputExt = targetFormat ? outputFormat : inputExt;
978 const {
979 dir: fileDir,
980 name: fileName
981 } = path.parse(original.filename);
982 const {
983 width,
984 height
985 } = result.info;
986 const sizeSuffix = typeof minimizerOptions.sizeSuffix === "function" ? minimizerOptions.sizeSuffix(width, height) : "";
987 const filename = path.join(fileDir, `${fileName}${sizeSuffix}.${outputExt}`);
988 const info = action === 0 ? { ...original.info,
989 minimized: true,
990 minimizedBy: original.info && original.info.generatedBy ? ["sharp", ...original.info.generatedBy] : ["sharp"]
991 } : { ...original.info,
992 generated: true,
993 generatedBy: original.info && original.info.generatedBy ? ["sharp", ...original.info.generatedBy] : ["sharp"]
994 };
995 return {
996 filename,
997 data: result.data,
998 warnings: [...original.warnings],
999 errors: [...original.errors],
1000 info
1001 };
1002}
1003/**
1004 * @template T
1005 * @param {WorkerResult} original
1006 * @param {T} minimizerOptions
1007 * @returns {Promise<WorkerResult>}
1008 */
1009
1010
1011function sharpGenerate(original, minimizerOptions) {
1012 var _squooshOptions$encod;
1013
1014 const squooshOptions =
1015 /** @type {SharpOptions} */
1016 minimizerOptions || {};
1017 const targetFormats =
1018 /** @type {SharpFormat[]} */
1019 Object.keys((_squooshOptions$encod = squooshOptions.encodeOptions) !== null && _squooshOptions$encod !== void 0 ? _squooshOptions$encod : {});
1020
1021 if (targetFormats.length === 0) {
1022 const error = new Error(`No result from 'sharp' for '${original.filename}', please configure the 'encodeOptions' option to generate images`);
1023 original.info.original = true;
1024 original.errors.push(error);
1025 return Promise.resolve(original);
1026 }
1027
1028 if (targetFormats.length > 1) {
1029 const error = new Error(`Multiple values for the 'encodeOptions' option is not supported for '${original.filename}', specify only one codec for the generator`);
1030 original.info.original = true;
1031 original.errors.push(error);
1032 return Promise.resolve(original);
1033 }
1034
1035 const [targetFormat] = targetFormats;
1036 return sharpTransform(original, 1, squooshOptions, targetFormat);
1037}
1038/**
1039 * @template T
1040 * @param {WorkerResult} original
1041 * @param {T} options
1042 * @returns {Promise<WorkerResult>}
1043 */
1044
1045
1046function sharpMinify(original, options) {
1047 return sharpTransform(original, 0, options);
1048}
1049
1050module.exports = {
1051 throttleAll,
1052 isAbsoluteURL,
1053 imageminNormalizeConfig,
1054 imageminMinify,
1055 imageminGenerate,
1056 squooshMinify,
1057 squooshGenerate,
1058 sharpMinify,
1059 sharpGenerate
1060};
\No newline at end of file