module.exports = function(RED) { "use strict"; const pureimage = require("pureimage"); const Readable = require("stream").Readable; const Writable = require("stream").Writable; const path = require("path"); let fontLoaded = false; function loadFont() { if (!fontLoaded) { const fnt = pureimage.registerFont(path.join(__dirname,'./SourceSansPro-Regular.ttf'),'Source Sans Pro'); fnt.load(); fontLoaded = true; } } function AnnotateNode(n) { RED.nodes.createNode(this,n); var node = this; const defaultFill = n.fill || ""; const defaultStroke = n.stroke || "#ffC000"; const defaultLineWidth = parseInt(n.lineWidth) || 5; const defaultFontSize = n.fontSize || 24; const defaultFontColor = n.fontColor || "#ffC000"; loadFont(); this.on("input", function(msg) { if (Buffer.isBuffer(msg.payload)) { if (msg.payload[0] !== 0xFF || msg.payload[1] !== 0xD8) { node.error("Not a JPEG image",msg); } else if (Array.isArray(msg.annotations) && msg.annotations.length > 0) { const stream = new Readable(); stream.push(msg.payload); stream.push(null); pureimage.decodeJPEGFromStream(stream).then(img => { const c = pureimage.make(img.width, img.height); const ctx = c.getContext('2d'); ctx.drawImage(img,0,0,img.width,img.height); ctx.lineJoin = 'bevel'; msg.annotations.forEach(function(annotation) { ctx.fillStyle = annotation.fill || defaultFill; ctx.strokeStyle = annotation.stroke || defaultStroke; ctx.lineWidth = annotation.lineWidth || defaultLineWidth; ctx.lineJoin = 'bevel'; let x,y,r,w,h; if (!annotation.type && annotation.bbox) { annotation.type = 'rect'; } switch(annotation.type) { case 'rect': if (annotation.bbox) { x = annotation.bbox[0] y = annotation.bbox[1] w = annotation.bbox[2] h = annotation.bbox[3] } else { x = annotation.x y = annotation.y w = annotation.w h = annotation.h } if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } ctx.beginPath(); ctx.lineWidth = annotation.lineWidth || defaultLineWidth; ctx.rect(x,y,w,h); ctx.closePath(); ctx.stroke(); if (annotation.label) { ctx.font = `${annotation.fontSize || defaultFontSize}pt 'Source Sans Pro'`; ctx.fillStyle = annotation.fontColor || defaultFontColor; ctx.textBaseline = "top"; ctx.textAlign = "left"; //set offset value so txt is above or below image if (annotation.labelLocation) { if (annotation.labelLocation === "top") { y = y - (20+((defaultLineWidth*0.5)+(Number(defaultFontSize)))); if (y < 0) { y = 0; } } else if (annotation.labelLocation === "bottom") { y = y + (10+h+(((defaultLineWidth*0.5)+(Number(defaultFontSize))))); ctx.textBaseline = "bottom"; } } //if not user defined make best guess for top or bottom based on location else { //not enought room above imagebox, put label on the bottom if (y < 0 + (20+((defaultLineWidth*0.5)+(Number(defaultFontSize))))) { y = y + (10+h+(((defaultLineWidth*0.5)+(Number(defaultFontSize))))); ctx.textBaseline = "bottom"; } //else put the label on the top else { y = y - (20+((defaultLineWidth*0.5)+(Number(defaultFontSize)))); if (y < 0) { y = 0; } } } ctx.fillText(annotation.label, x,y); } break; case 'circle': if (annotation.bbox) { x = annotation.bbox[0] + annotation.bbox[2]/2 y = annotation.bbox[1] + annotation.bbox[3]/2 r = Math.min(annotation.bbox[2],annotation.bbox[3])/2; } else { x = annotation.x y = annotation.y r = annotation.r; } ctx.beginPath(); ctx.lineWidth = annotation.lineWidth || defaultLineWidth; ctx.arc(x,y,r,0,Math.PI*2); ctx.closePath(); ctx.stroke(); if (annotation.label) { ctx.font = `${annotation.fontSize || defaultFontSize}pt 'Source Sans Pro'`; ctx.fillStyle = annotation.fontColor || defaultFontColor; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.fillText(annotation.label, x+2,y) } break; } }); const bufferOutput = getWritableBuffer(); pureimage.encodeJPEGToStream(c,bufferOutput.stream,90).then(() => { msg.payload = bufferOutput.getBuffer(); node.send(msg); }) }).catch(err => { node.error(err,msg); }) } else { // No annotations to make - send the message on node.send(msg); } } else { node.error("Payload not a Buffer",msg) } return msg; }); } RED.nodes.registerType("annotate-image",AnnotateNode); function getWritableBuffer() { var currentSize = 0; var buffer = null; const stream = new Writable({ write(chunk, encoding, callback) { if (!buffer) { buffer = Buffer.from(chunk); } else { var newBuffer = Buffer.allocUnsafe(currentSize + chunk.length); buffer.copy(newBuffer); chunk.copy(newBuffer,currentSize); buffer = newBuffer; } currentSize += chunk.length callback(); } }); return { stream: stream, getBuffer: function() { return buffer; } } } }