@rosepetal/node-red-contrib-utils 1.1.2 → 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
  }
@@ -102,8 +102,7 @@ module.exports = function registerCleanDebugNode(RED) {
102
102
  return;
103
103
  }
104
104
 
105
- const format = detectValueType(value);
106
- const debugMessage = {
105
+ var debugMessage = {
107
106
  id: node.id,
108
107
  z: node.z,
109
108
  _alias: node._alias,
@@ -112,13 +111,45 @@ module.exports = function registerCleanDebugNode(RED) {
112
111
  property: node.targetType === "full" ? "msg" : node.complete,
113
112
  propertyType: node.targetType,
114
113
  clean: node.clean,
115
- format,
116
114
  msg: value,
117
115
  _msgid: msg && msg._msgid
118
116
  };
119
117
 
118
+ encodeDebugValue(debugMessage);
119
+
120
120
  RED.comms.publish("debug", debugMessage);
121
121
  }
122
+
123
+ /**
124
+ * Encode debugMsg.msg and set debugMsg.format in-place so the value
125
+ * matches the format expected by Node-RED's debug-utils.js in the editor.
126
+ * RED.util.encodeObject expects the whole debug message object — it reads
127
+ * from debugMsg.msg and sets debugMsg.msg + debugMsg.format in-place.
128
+ * @param {object} debugMsg
129
+ */
130
+ function encodeDebugValue(debugMsg) {
131
+ var value = debugMsg.msg;
132
+ if (value === null || typeof value === "undefined") {
133
+ debugMsg.format = (value === null) ? "null" : "undefined";
134
+ debugMsg.msg = (value === null) ? "null" : "(undefined)";
135
+ } else if (typeof value === "object") {
136
+ try {
137
+ RED.util.encodeObject(debugMsg, { maxLength: DEFAULT_DEBUG_MAX_LENGTH });
138
+ } catch (_e) {
139
+ debugMsg.format = "error";
140
+ debugMsg.msg = "Failed to encode debug value";
141
+ }
142
+ } else if (typeof value === "boolean") {
143
+ debugMsg.format = "boolean";
144
+ debugMsg.msg = value.toString();
145
+ } else if (typeof value === "number") {
146
+ debugMsg.format = "number";
147
+ debugMsg.msg = value.toString();
148
+ } else {
149
+ debugMsg.format = "string[" + ("" + value).length + "]";
150
+ debugMsg.msg = "" + value;
151
+ }
152
+ }
122
153
  }
123
154
 
124
155
  /**
@@ -330,30 +361,6 @@ module.exports = function registerCleanDebugNode(RED) {
330
361
  return `[${type} withheld: ${detail}]`;
331
362
  }
332
363
 
333
- /**
334
- * Detect broad value type to help the editor render icons.
335
- * @param {*} value
336
- */
337
- function detectValueType(value) {
338
- if (value === null) {
339
- return "null";
340
- }
341
- if (value && typeof value === "object" && value.type === "Buffer" && Array.isArray(value.data)) {
342
- return "buffer";
343
- }
344
- const t = typeof value;
345
- if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
346
- return t;
347
- }
348
- if (Array.isArray(value)) {
349
- return "array";
350
- }
351
- if (value instanceof Date) {
352
- return "date";
353
- }
354
- return "object";
355
- }
356
-
357
364
  /**
358
365
  * Format for console/log output.
359
366
  * @param {*} value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosepetal/node-red-contrib-utils",
3
- "version": "1.1.2",
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"