UNPKG

@droneblocks/node-red-dexi

Version:

You want to make sure you don't lose any of your flow development. Make sure to map the host "flows" directory to the container directory as shown below. This will store the flow on your host machine if/when the container is destroyed.

224 lines (204 loc) 9.72 kB
<script type="text/javascript"> (function() { const DEFAULT_IMAGE_WIDTH = 320 RED.nodes.registerType('save-photo', { category: 'DEXI', color: '#88A882', defaults: { name: { value: "" }, width: { value: DEFAULT_IMAGE_WIDTH, required: true, validate: function (v) { return !v || !isNaN(parseInt(v, 10)) } }, filename: { value: "dexi_photo" }, autoSave: { value: false }, showPreview: { value: true } }, inputs: 1, outputs: 1, icon: "font-awesome/fa-camera", align: 'right', palettelabel: "save photo", label: function () { return this.name || "save photo"; }, labelStyle: function () { return this.name ? "node_label_italic" : ""; }, inputLabels: "image data", button: { enabled: function() { return !this.autoSave; }, visible: function() { return !this.autoSave; }, onclick: function () { const nodeId = this.id; const latestImage = latestImages[nodeId]; if (latestImage) { downloadImage(latestImage, this.filename, 'jpg'); RED.notify("Image downloaded", "success"); } else { RED.notify("No image available to download", "warning"); } } }, oneditprepare: function () { $('#node-input-width').val(this.width || DEFAULT_IMAGE_WIDTH); } }); const latestImages = {} function downloadImage(imageData, filename) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fullFilename = `${filename}_${timestamp}.jpg`; let dataUrl; if (imageData.startsWith("data:")) { dataUrl = imageData; } else if (imageData.startsWith("http")) { // For URLs, we'd need to fetch and convert - for now just open in new tab window.open(imageData, '_blank'); return; } else { // Assume base64 JPEG from compressed topic dataUrl = `data:image/jpeg;base64,${imageData}`; } // Create download link const link = document.createElement('a'); link.href = dataUrl; link.download = fullFilename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } var remove = function(nodeid) { const id = nodeid const $img = document.getElementById("save-photo-img-" + id) const $bubble = document.getElementById("save-photo-bubble-" + id) const $button = document.getElementById("save-photo-btn-" + id) $img && $img.remove() $bubble && $bubble.remove() $button && $button.remove() delete latestImages[id] } var redraw = function(node) { const id = node.id remove(id) if (latestImages[id] && node.showPreview) { render(id, latestImages[id], node) } } var render = function(id, data, node) { if (!node.showPreview) return; let i = new Image(); let $img = document.getElementById("camera-capture-img-" + id) if (!$img) { const $container = document.getElementById(id) if (!$container) { return } const bubble = document.createElementNS("http://www.w3.org/2000/svg", 'polyline') bubble.setAttribute('id', "save-photo-bubble-" + id) bubble.setAttribute('style', 'fill:#E8F0E8') bubble.setAttribute('stroke', '#999999') $container.insertBefore(bubble, $container.lastChild.nextSibling) const img = document.createElementNS("http://www.w3.org/2000/svg", 'image') img.setAttribute('id', "save-photo-img-" + id) img.setAttribute('x', '0') img.setAttribute('y', '45') img.setAttribute('width', node.width || DEFAULT_IMAGE_WIDTH) $container.insertBefore(img, $container.lastChild.nextSibling) // Add download button overlay if (!node.autoSave) { const downloadBtn = document.createElementNS("http://www.w3.org/2000/svg", 'text') downloadBtn.setAttribute('id', "save-photo-btn-" + id) downloadBtn.setAttribute('x', '5') downloadBtn.setAttribute('y', '60') downloadBtn.setAttribute('fill', 'white') downloadBtn.setAttribute('font-size', '12') downloadBtn.setAttribute('cursor', 'pointer') downloadBtn.textContent = '📥' downloadBtn.addEventListener("click", function() { downloadImage(latestImages[id], node.filename); }) $container.insertBefore(downloadBtn, $container.lastChild.nextSibling) } $img = img } i.onload = function () { $img.setAttribute('height', node.width * i.height / i.width) const bubbleId = $img.id.replace("img", "bubble"); const $bubble = document.getElementById(bubbleId) let imgBbox = {} imgBbox.x = 0; imgBbox.y = 45; imgBbox.width = node.width || DEFAULT_IMAGE_WIDTH; imgBbox.height = parseInt(node.width * i.height / i.width); const left = imgBbox.x const top = imgBbox.y + 2 const right = imgBbox.x + imgBbox.width const bottom = imgBbox.y + imgBbox.height const points = (left + 4) + "," + (top - 17) + " " + (left + 4) + "," + top + " " + right + "," + top + " " + right + "," + bottom + " " + left + "," + bottom + " " + left + "," + (top - 21) $bubble.setAttribute('points', points) } if (data.startsWith("http")) { $img.setAttribute('href', data); i.src = data; } else { $img.setAttribute('href', "data:image/png;base64," + data); i.src = "data:image/png;base64," + data; } } RED.events.on("editor:save", redraw) RED.comms.subscribe('save-photo', function (event, data) { if (data.hasOwnProperty("data")) { latestImages[data.id] = data.data const node = RED.nodes.node(data.id) if (node) { render(data.id, data.data, node) // Auto-download if enabled if (node.autoSave) { downloadImage(data.data, node.filename); } } } else { remove(data.id); } }) })(); </script> <script type="text/html" data-template-name="save-photo"> <div class="form-row"> <label for="node-input-width"><i class="fa fa-arrows-h"></i> Width</label> <input type="number" id="node-input-width"> </div> <div class="form-row"> <label for="node-input-filename"><i class="fa fa-file"></i> Filename</label> <input type="text" id="node-input-filename" placeholder="dexi_photo"> </div> <div class="form-row"> <label>&nbsp;</label> <input type="checkbox" id="node-input-autoSave" style="display:inline-block; width:auto; vertical-align:top;"> <label for="node-input-autoSave" style="width:70%;"> Auto-save images</label> </div> <div class="form-row"> <label>&nbsp;</label> <input type="checkbox" id="node-input-showPreview" style="display:inline-block; width:auto; vertical-align:top;"> <label for="node-input-showPreview" style="width:70%;"> Show image preview</label> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <input type="text" id="node-input-name" placeholder="Name"> </div> </script> <script type="text/html" data-help-name="save-photo"> <p>Save photo node that accepts image data from input messages and saves JPG images directly to your computer via browser downloads.</p> <p><strong>Input:</strong> Expects image data in the input message payload. Supports base64-encoded images or ROS image message formats.</p> <p><strong>Auto-save:</strong> When enabled, images are automatically downloaded when received.</p> <p><strong>Manual save:</strong> When auto-save is disabled, click the node button or the download icon on the image to save.</p> <p><strong>Usage:</strong> Connect a ros2-subscribe node or other image source to provide image data to this node.</p> <p><strong>Note:</strong> All images are saved as JPG format with timestamp in filename.</p> </script>