@localnerve/gulp-images 0.1.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/LICENSE.md +660 -0
- package/README.md +150 -0
- package/package.json +56 -0
- package/src/index.js +20 -0
- package/src/optimize/index.js +14 -0
- package/src/optimize/jpeg.js +48 -0
- package/src/optimize/png.js +48 -0
- package/src/optimize/svg.js +49 -0
- package/src/responsive/index.js +65 -0
- package/src/transform/index.js +12 -0
- package/src/transform/toWebp.js +86 -0
- package/src/utils.js +143 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @localnerve/gulp-images — shared utilities.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Alex Grant <info@localnerve.com> (https://www.localnerve.com), LocalNerve LLC
|
|
5
|
+
* AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { Transform } from 'node:stream';
|
|
10
|
+
import PluginError from 'plugin-error';
|
|
11
|
+
import { simd, relaxedSimd } from 'wasm-feature-detect';
|
|
12
|
+
import decodeJpeg, { init as initJpegDecode } from '@jsquash/jpeg/decode.js';
|
|
13
|
+
import encodeJpeg, { init as initJpegEncode } from '@jsquash/jpeg/encode.js';
|
|
14
|
+
import decodePng, { init as initPngDecode } from '@jsquash/png/decode.js';
|
|
15
|
+
import encodePng, { init as initPngEncode } from '@jsquash/oxipng/optimise.js';
|
|
16
|
+
import encodeWebp, { init as initWebpEncode } from '@jsquash/webp/encode.js';
|
|
17
|
+
|
|
18
|
+
export { decodeJpeg, encodeJpeg, decodePng, encodePng, encodeWebp };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check skip condition for a vinyl stream object.
|
|
22
|
+
* Skip if: not the right extension, empty file, or a stream/null.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('vinyl')} file - Vinyl file object passing through
|
|
25
|
+
* @param {string[]} exts - Array of dot-prefixed extensions to allow, e.g. ['.jpg']
|
|
26
|
+
* @returns {boolean} true if the file should be skipped
|
|
27
|
+
*/
|
|
28
|
+
export function checkSkip(file, exts) {
|
|
29
|
+
return (
|
|
30
|
+
!exts.includes(file.extname.toLowerCase()) ||
|
|
31
|
+
!file.contents.toString('utf8') ||
|
|
32
|
+
file.isStream() ||
|
|
33
|
+
file.isNull()
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Colorized console logger.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} owner - The plugin/function name
|
|
41
|
+
* @param {import('vinyl')} file - Vinyl file object
|
|
42
|
+
* @param {string} message - The log message
|
|
43
|
+
* @param {'log'|'error'} [method='log'] - console method to use
|
|
44
|
+
*/
|
|
45
|
+
export function log(owner, file, message, method = 'log') {
|
|
46
|
+
const colors = {
|
|
47
|
+
magenta: '\x1b[35m',
|
|
48
|
+
yellow: '\x1b[33m',
|
|
49
|
+
red: '\x1b[31m',
|
|
50
|
+
green: '\x1b[32m',
|
|
51
|
+
reset: '\x1b[0m'
|
|
52
|
+
};
|
|
53
|
+
const filepath = path.relative(process.cwd(), file.path);
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const TN = i => i < 10 ? `0${i}` : i;
|
|
56
|
+
const timestring = `${TN(now.getHours())}:${TN(now.getMinutes())}:${TN(now.getSeconds())}`;
|
|
57
|
+
|
|
58
|
+
console[method](
|
|
59
|
+
`[${colors.magenta}${timestring}${colors.reset}] \n${owner}: ${method === 'log' ? colors.green : colors.red}File ${filepath} - ${colors.yellow}${message}${colors.reset}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Handle an error inside a gulp transform stream.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} owner - The plugin/function name
|
|
67
|
+
* @param {import('vinyl')} file - Vinyl file object
|
|
68
|
+
* @param {Function} next - The transform callback
|
|
69
|
+
* @param {Error|string} error - The error
|
|
70
|
+
*/
|
|
71
|
+
export function handleError(owner, file, next, error) {
|
|
72
|
+
const colors = { reset: '\x1b[0m' };
|
|
73
|
+
const filepath = path.relative(process.cwd(), file.path);
|
|
74
|
+
let message = error.message || error;
|
|
75
|
+
|
|
76
|
+
if (message) {
|
|
77
|
+
message = message
|
|
78
|
+
.replace('Line:', `${colors.reset}File: ${filepath}\nLine:`)
|
|
79
|
+
.replace(/\n/g, '\n\t')
|
|
80
|
+
.trim();
|
|
81
|
+
log(owner, file, message, 'error');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
next(new PluginError(owner, message));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pass-through transform (no-op).
|
|
89
|
+
* Returned by optimize functions when not in production mode.
|
|
90
|
+
*
|
|
91
|
+
* @returns {Transform}
|
|
92
|
+
*/
|
|
93
|
+
export function passThrough() {
|
|
94
|
+
return new Transform({
|
|
95
|
+
objectMode: true,
|
|
96
|
+
transform: (file, enc, next) => next(null, file)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// WASM codec paths relative to a base directory
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
const WASM_PATHS = {
|
|
104
|
+
jpegDecode: 'node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm',
|
|
105
|
+
jpegEncode: 'node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm',
|
|
106
|
+
pngDecode: 'node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm',
|
|
107
|
+
pngEncode: 'node_modules/@jsquash/oxipng/codec/pkg/squoosh_oxipng_bg.wasm',
|
|
108
|
+
webpEncode: 'node_modules/@jsquash/webp/codec/enc/webp_enc.wasm',
|
|
109
|
+
webpEncodeSIMD: 'node_modules/@jsquash/webp/codec/enc/webp_enc_simd.wasm'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Initialize all WASM codec modules.
|
|
114
|
+
* Call this once before running any image transforms.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} [wasmBasePath] - Absolute path to the directory that contains
|
|
117
|
+
* the `node_modules` folder where the @jsquash packages are installed.
|
|
118
|
+
* Defaults to `process.cwd()`.
|
|
119
|
+
*/
|
|
120
|
+
export async function initWasmModules(wasmBasePath = process.cwd()) {
|
|
121
|
+
const resolve = rel => path.join(wasmBasePath, rel);
|
|
122
|
+
|
|
123
|
+
const jpegDecWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.jpegDecode)));
|
|
124
|
+
await initJpegDecode(jpegDecWasmModule);
|
|
125
|
+
|
|
126
|
+
const jpegEncWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.jpegEncode)));
|
|
127
|
+
await initJpegEncode(jpegEncWasmModule);
|
|
128
|
+
|
|
129
|
+
const pngDecWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.pngDecode)));
|
|
130
|
+
await initPngDecode(pngDecWasmModule);
|
|
131
|
+
|
|
132
|
+
const oxipngWasmModule = await WebAssembly.compile(await fs.readFile(resolve(WASM_PATHS.pngEncode)));
|
|
133
|
+
await initPngEncode(oxipngWasmModule);
|
|
134
|
+
|
|
135
|
+
// use SIMD variant if supported (~10 % faster)
|
|
136
|
+
const simdSupport = await Promise.allSettled([simd(), relaxedSimd()]);
|
|
137
|
+
const webpPath = simdSupport.some(r => r.status === 'fulfilled' && r.value)
|
|
138
|
+
? WASM_PATHS.webpEncodeSIMD
|
|
139
|
+
: WASM_PATHS.webpEncode;
|
|
140
|
+
|
|
141
|
+
const webpEncWasmModule = await WebAssembly.compile(await fs.readFile(resolve(webpPath)));
|
|
142
|
+
await initWebpEncode(webpEncWasmModule);
|
|
143
|
+
}
|