@rosepetal/node-red-contrib-utils 1.1.3 → 1.1.4

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.
@@ -14,6 +14,9 @@
14
14
  imageFormat: { value: "jpg" },
15
15
  imageQuality: { value: 90 },
16
16
  pngOptimize: { value: false },
17
+ webpLossless: { value: false },
18
+ webpSmartSubsample: { value: false },
19
+ webpEffort: { value: 4 },
17
20
  jsonIndent: { value: 2 },
18
21
  overwriteProtection: { value: "true" },
19
22
  outputPath: { value: "savedFile" },
@@ -63,6 +66,7 @@
63
66
  const imageFields = $(".savefile-image-options");
64
67
  const jsonFields = $(".savefile-json-options");
65
68
  const pngOnlyFields = $(".savefile-png-only");
69
+ const webpOnlyFields = $(".savefile-webp-only");
66
70
  const qualityFields = $(".savefile-quality");
67
71
  const imageSeparators = $(".savefile-image-separator");
68
72
  const jsonSeparators = $(".savefile-json-separator");
@@ -80,6 +84,7 @@
80
84
  const fmt = imageFormatField.val();
81
85
  const showImage = fileTypeField.val() === 'image' || fileTypeField.val() === 'auto';
82
86
  pngOnlyFields.toggle(showImage && fmt === 'png');
87
+ webpOnlyFields.toggle(showImage && fmt === 'webp');
83
88
  qualityFields.toggle(showImage && fmt !== 'png');
84
89
  }
85
90
 
@@ -169,6 +174,21 @@
169
174
  <input type="checkbox" id="node-input-pngOptimize" style="margin-right: 5px;"> Optimize PNG
170
175
  </label>
171
176
  </div>
177
+ <div class="form-row savefile-image-options savefile-webp-only">
178
+ <label for="node-input-webpLossless"><i class="fa fa-lock"></i> WebP Lossless</label>
179
+ <input type="checkbox" id="node-input-webpLossless" style="display:inline-block; width:auto; vertical-align:baseline;">
180
+ <label for="node-input-webpLossless" style="width:auto; margin-left:5px;">Encode WebP without loss</label>
181
+ </div>
182
+ <div class="form-row savefile-image-options savefile-webp-only">
183
+ <label for="node-input-webpSmartSubsample"><i class="fa fa-magic"></i> WebP Smart Subsample</label>
184
+ <input type="checkbox" id="node-input-webpSmartSubsample" style="display:inline-block; width:auto; vertical-align:baseline;">
185
+ <label for="node-input-webpSmartSubsample" style="width:auto; margin-left:5px;">Improve chroma downsampling (lossy WebP only)</label>
186
+ </div>
187
+ <div class="form-row savefile-image-options savefile-webp-only">
188
+ <label for="node-input-webpEffort"><i class="fa fa-tachometer"></i> WebP Effort</label>
189
+ <input type="number" id="node-input-webpEffort" min="0" max="6" placeholder="4" style="width: 70px;">
190
+ <span style="margin-left: 10px; color: #666;">0-6 (higher = smaller file, slower)</span>
191
+ </div>
172
192
  <div class="form-row savefile-image-options savefile-separator savefile-image-separator">
173
193
  <hr>
174
194
  </div>
@@ -225,6 +245,8 @@
225
245
  <dd>Force saving as image, JSON, text, or binary, or let the node auto-detect from data/extension.</dd>
226
246
  <dt>Image Format / Quality / Optimize PNG <span class="property-type">enum/number/bool</span></dt>
227
247
  <dd>Applies when saving images. Raw image objects are validated and encoded with Sharp.</dd>
248
+ <dt>WebP Lossless / Smart Subsample / Effort <span class="property-type">bool/bool/number</span></dt>
249
+ <dd>WebP-specific encoding options. <b>Lossless</b> encodes without quality loss. <b>Smart Subsample</b> improves chroma downsampling for lossy WebP. <b>Effort</b> (0-6) controls the compression effort trade-off; higher values produce smaller files but encode more slowly.</dd>
228
250
  <dt>JSON Indent <span class="property-type">number</span></dt>
229
251
  <dd>Spacing for pretty-printed JSON output.</dd>
230
252
  <dt>Overwrite <span class="property-type">enum</span></dt>
@@ -136,6 +136,10 @@ module.exports = function (RED) {
136
136
  // Prepare data to write
137
137
  const quality = parseInt(config.imageQuality, 10) || 90;
138
138
  const pngOptimize = config.pngOptimize === true || config.pngOptimize === 'true';
139
+ const webpLossless = config.webpLossless === true || config.webpLossless === 'true';
140
+ const webpSmartSubsample = config.webpSmartSubsample === true || config.webpSmartSubsample === 'true';
141
+ const webpEffort = parseInt(config.webpEffort, 10);
142
+ const webpOptions = { lossless: webpLossless, smartSubsample: webpSmartSubsample, effort: webpEffort };
139
143
  const jsonIndent = Number.isInteger(parseInt(config.jsonIndent, 10))
140
144
  ? parseInt(config.jsonIndent, 10)
141
145
  : 2;
@@ -144,7 +148,7 @@ module.exports = function (RED) {
144
148
  let isText = false;
145
149
 
146
150
  if (fileType === 'image') {
147
- payloadToWrite = await toImageBuffer(incoming, imageFormat, quality, pngOptimize, NodeUtils, node);
151
+ payloadToWrite = await toImageBuffer(incoming, imageFormat, quality, pngOptimize, webpOptions, NodeUtils, node);
148
152
  } else if (fileType === 'json') {
149
153
  if (typeof incoming === 'string') {
150
154
  try {
@@ -341,7 +345,7 @@ async function toBinary(value) {
341
345
  return Buffer.from(JSON.stringify(value ?? ''), 'utf8');
342
346
  }
343
347
 
344
- async function toImageBuffer(imageValue, format, quality, pngOptimize, NodeUtils, node) {
348
+ async function toImageBuffer(imageValue, format, quality, pngOptimize, webpOptions, NodeUtils, node) {
345
349
  // Encoded buffer path
346
350
  if (Buffer.isBuffer(imageValue) && !(imageValue.width && imageValue.height)) {
347
351
  let inputFormat;
@@ -357,7 +361,7 @@ async function toImageBuffer(imageValue, format, quality, pngOptimize, NodeUtils
357
361
  }
358
362
 
359
363
  const sharpInstance = sharp(imageValue);
360
- return encodeSharp(sharpInstance, format, quality, pngOptimize);
364
+ return encodeSharp(sharpInstance, format, quality, pngOptimize, webpOptions);
361
365
  }
362
366
 
363
367
  // Raw image path (uses validator for clear errors)
@@ -391,10 +395,10 @@ async function toImageBuffer(imageValue, format, quality, pngOptimize, NodeUtils
391
395
  sharpInstance = sharpInstance.toColourspace('b-w');
392
396
  }
393
397
 
394
- return encodeSharp(sharpInstance, format, quality, pngOptimize);
398
+ return encodeSharp(sharpInstance, format, quality, pngOptimize, webpOptions);
395
399
  }
396
400
 
397
- function encodeSharp(sharpInstance, format, quality, pngOptimize) {
401
+ function encodeSharp(sharpInstance, format, quality, pngOptimize, webpOptions) {
398
402
  if (format === 'png') {
399
403
  const pngOptions = pngOptimize
400
404
  ? { compressionLevel: 9, palette: true }
@@ -402,7 +406,15 @@ function encodeSharp(sharpInstance, format, quality, pngOptimize) {
402
406
  return sharpInstance.png(pngOptions).toBuffer();
403
407
  }
404
408
  if (format === 'webp') {
405
- return sharpInstance.webp({ quality }).toBuffer();
409
+ const opts = { quality };
410
+ if (webpOptions) {
411
+ if (webpOptions.lossless) opts.lossless = true;
412
+ if (webpOptions.smartSubsample) opts.smartSubsample = true;
413
+ if (typeof webpOptions.effort === 'number' && webpOptions.effort >= 0 && webpOptions.effort <= 6) {
414
+ opts.effort = webpOptions.effort;
415
+ }
416
+ }
417
+ return sharpInstance.webp(opts).toBuffer();
406
418
  }
407
419
  return sharpInstance.jpeg({ quality }).toBuffer();
408
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosepetal/node-red-contrib-utils",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Utility and I/O nodes for Node-RED, including array helpers and file saving.",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"