diff --git a/src/nodes/ApiJson.ts b/src/nodes/ApiJson.ts new file mode 100644 index 0000000..2460369 --- /dev/null +++ b/src/nodes/ApiJson.ts @@ -0,0 +1,145 @@ +import { + NumberVar, + StringVar, + BooleanVar, +} from './io/AbstractIOSet'; +import Node from './Node'; +import _ from 'lodash'; + +const inputs = { + propertyPath0: { + type: 'String', + } as StringVar, + propertyPath1: { + type: 'String', + } as StringVar, + propertyPath2: { + type: 'String', + } as StringVar, + propertyPath3: { + type: 'String', + } as StringVar, + propertyPath4: { + type: 'String', + } as StringVar, + propertyPath5: { + type: 'String', + } as StringVar, + propertyPath6: { + type: 'String', + } as StringVar, + propertyPath7: { + type: 'String', + } as StringVar, + url: { + type: 'String', + } as StringVar, + payload: { + type: 'String', + } as StringVar, + method: { + type: 'String', + default: 'GET', + enum: ['GET', 'POST', 'PUT', 'DELETE'], + } as StringVar, + throttleMs: { + type: 'Number', + default: 10000, + } as NumberVar, + compress: { + type: 'Boolean', + default: false, + } as BooleanVar, + quality: { + type: 'Number', + default: 95, + min: 0, + max: 100, + } as NumberVar, +}; + +const outputs = { + value0: { + type: 'String', + } as StringVar, + value1: { + type: 'String', + } as StringVar, + value2: { + type: 'String', + } as StringVar, + value3: { + type: 'String', + } as StringVar, + value4: { + type: 'String', + } as StringVar, + value5: { + type: 'String', + } as StringVar, + value6: { + type: 'String', + } as StringVar, + value7: { + type: 'String', + } as StringVar, +}; + +export default class ApiJson extends Node { + private fetchDataThrottled; + private throttleWaitMs; + + constructor(options = {}) { + super('ApiJson', inputs, outputs); + this.throttleWaitMs = this.in.throttleMs.value; + this.fetchDataThrottled = _.throttle(this.fetchData, this.throttleWaitMs); + } + + async _update() { + const url = this.in.url.value; + + + if (!url) { + return; + } + + if (this.throttleWaitMs !== this.in.throttleMs.value) { + this.throttleWaitMs = this.in.throttleMs.value; + this.fetchDataThrottled = _.throttle(this.fetchData, this.throttleWaitMs); + } + + let data: object; + try { + data = await this.fetchDataThrottled(); + } catch (e) { + console.error(e); + await new Promise((resolve) => setTimeout(resolve, this.throttleWaitMs)); + return; + } + + this.out.value0.value = _.get(data, this.in.propertyPath0.value); + this.out.value1.value = _.get(data, this.in.propertyPath1.value); + this.out.value2.value = _.get(data, this.in.propertyPath2.value); + this.out.value3.value = _.get(data, this.in.propertyPath3.value); + this.out.value4.value = _.get(data, this.in.propertyPath4.value); + this.out.value5.value = _.get(data, this.in.propertyPath5.value); + this.out.value6.value = _.get(data, this.in.propertyPath6.value); + this.out.value7.value = _.get(data, this.in.propertyPath7.value); + } + + async fetchData() { + const response = await fetch(this.in.url.value, { + method: this.in.method.value, + headers: { + 'Content-Type': 'application/json', + }, + ...(this.in.payload.value ? {body: this.in.payload.value} : {}), + }); + + if (response.status >= 400) { + throw new Error(response.statusText); + } + + return await response.json(); + } +} diff --git a/src/nodes/Base64ToImg.ts b/src/nodes/Base64ToImg.ts new file mode 100644 index 0000000..6873358 --- /dev/null +++ b/src/nodes/Base64ToImg.ts @@ -0,0 +1,84 @@ +import { + ImageVar, + NumberVar, + StringVar, +} from './io/AbstractIOSet'; +import Node from './Node'; +import {canvasPool2D} from '../canvas/CanvasPool'; + +const inputs = { + base64: { + type: 'String', + } as StringVar, +}; + +const outputs = { + image: { + type: 'Image', + } as ImageVar, + width: { + type: 'Number', + } as NumberVar, + height: { + type: 'Number', + } as NumberVar, +}; + +/** + * TODO: after successful tests move this to the library + */ +export default class Base64ToImg extends Node { + constructor(options = {}) { + super('Base64ToImg', inputs, outputs); + } + + async _update() { + let base64String = this.in.base64.value; + + if (!base64String) { + return; + } + + if (!base64String.startsWith('data:image/')) { + base64String = `data:${this.estimateImageMimeType(base64String)};base64,${base64String}`; + } + + const canvas = canvasPool2D.createCanvas(); + canvas.acquire(); + + await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + const canvasContext = canvas.getContext('2d'); + canvasContext.drawImage(img, 0, 0); + resolve(img); + }; + + img.onerror = (e) => { + reject(new Error(e as string)); + }; + + img.src = base64String; + }); + + this.out.image.value = canvas; + this.out.width.value = canvas.width; + this.out.height.value = canvas.height; + } + + private estimateImageMimeType(base64String: string): string { + // Attempt to estimate the MIME type based on base64 signature. + if (base64String.startsWith('iVBORw0KGgo=')) { + return 'image/png'; + } else if (base64String.startsWith('/9j/')) { + return 'image/jpeg'; + } else if (base64String.startsWith('UklGR')) { + return 'image/webp'; + } else { + // Default or unknown type + return 'application/octet-stream'; + } + } +} diff --git a/src/nodes/Eval.ts b/src/nodes/Eval.ts index 1c9e086..ead17b3 100644 --- a/src/nodes/Eval.ts +++ b/src/nodes/Eval.ts @@ -5,11 +5,11 @@ import { StringVar, BooleanVar, } from './io/AbstractIOSet'; -import get from 'lodash/get'; +import _ from 'lodash'; const inputs = { image: { - type: 'Image' + type: 'Image', } as ImageVar, i0: { type: 'Number', @@ -43,6 +43,38 @@ const inputs = { type: 'Number', default: 0, } as NumberVar, + i8: { + type: 'String', + default: undefined, + } as StringVar, + i9: { + type: 'String', + default: undefined, + } as StringVar, + i10: { + type: 'String', + default: undefined, + } as StringVar, + i11: { + type: 'String', + default: undefined, + } as StringVar, + i12: { + type: 'String', + default: undefined, + } as StringVar, + i13: { + type: 'String', + default: undefined, + } as StringVar, + i14: { + type: 'String', + default: undefined, + } as StringVar, + i15: { + type: 'String', + default: undefined, + } as StringVar, formula: { type: 'String', default: '', @@ -50,41 +82,52 @@ const inputs = { waitForInput: { type: 'Boolean', default: true, - } as BooleanVar + } as BooleanVar, }; const outputs = { image: { - type: 'Image' + type: 'Image', } as ImageVar, result: { - type: 'Number' + type: 'Number', } as NumberVar, -} + resultString: { + type: 'String', + } as StringVar, +}; export default class Eval extends Node { constructor() { super('Eval', inputs, outputs); } - interpolate() { - return this.in.formula.value.replace(/\{(\w+)\}/g, (_, paramName) => - get(this.in, [paramName, 'value'], 0) - ) + interpolate(defaultValue: any) { + return this.in.formula.value.replace(/\{((i\d)+)}/g, (originalString, paramName) => { + return _.get(this.in, [paramName, 'value'], defaultValue); + }); } hasAllConnectedInputsValue() { - if (this.in.waitForInput.value) { - return super.hasAllConnectedInputsValue(); + if (!this.in.waitForInput.value) { + return true; + } + + for (const inputName of Object.keys(this.in.variables)) { + // allow empty string + if (this.in[inputName].output && !this.in[inputName].value && !_.isString(this.in[inputName].value)) { + return false; + } } return true; } evaluate() { try { - this.out.result.value = eval(this.interpolate()); + this.out.result.value = eval(this.interpolate(0)); + this.out.resultString.value = eval(this.interpolate('')); } catch (e) { - console.error(e); + console.error(e, this.in); } this.out.image.value = this.in.image.value; } diff --git a/src/nodes/ImgToBase64.ts b/src/nodes/ImgToBase64.ts new file mode 100644 index 0000000..7ecb86c --- /dev/null +++ b/src/nodes/ImgToBase64.ts @@ -0,0 +1,62 @@ +import { + ImageVar, + StringVar, +} from './io/AbstractIOSet'; +import Node from './Node'; +import {canvasPool2D} from '../canvas/CanvasPool'; +import {waitForMedia} from '../utils'; +import {mediaSize} from './canvas'; + +const inputs = { + image: { + type: 'Image', + } as ImageVar, + type: { + type: 'String', + enum: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], + default: 'image/png', + } as StringVar, +}; + +const outputs = { + base64: { + type: 'String', + } as StringVar, +}; + +export default class ImgToBase64 extends Node { + constructor(options = {}) { + super('ImgToBase64', inputs, outputs); + } + + async _update() { + const media = this.in.image.value; + + if (!media) { + return; + } + await waitForMedia(media); + const {width, height} = mediaSize(media); + + const canvas = canvasPool2D.createCanvas(); + canvas.acquire(); + canvas.width = width; + canvas.height = height; + + // @ts-ignore + canvas.getContext('2d').drawImage(media, 0, 0, width, height); + + const blob = await canvas.convertToBlob({type: this.in.type.value}); + + this.out.base64.value = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + resolve(reader.result as string); + }; + reader.onerror = (e) => { + reject(e); + }; + reader.readAsDataURL(blob); + }); + } +}