diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 893c61aa33..336e27041a 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -82,8 +82,8 @@
- - + +
@@ -213,6 +213,10 @@ export default mixins( this.loadRemoteParameterOptions(); }, value () { + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true) { + // Do not set for color with alpha else wrong value gets displayed in field + return; + } this.tempValue = this.displayValue as string; }, }, @@ -274,6 +278,18 @@ export default mixins( returnValue = this.expressionValueComputed; } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && returnValue.charAt(0) === '#') { + // Convert the value to rgba that el-color-picker can display it correctly + const bigint = parseInt(returnValue.slice(1), 16); + const h = []; + h.push((bigint >> 24) & 255); + h.push((bigint >> 16) & 255); + h.push((bigint >> 8) & 255); + h.push((255 - bigint & 255) / 255); + + returnValue = 'rgba('+h.join()+')'; + } + if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') { const rows = this.getArgument('rows'); if (rows === undefined || rows === 1) { @@ -537,14 +553,35 @@ export default mixins( // Set focus on field setTimeout(() => { // @ts-ignore - (this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus(); + if (this.$refs.inputField.$el) { + // @ts-ignore + (this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus(); + } }); }, + rgbaToHex (value: string): string | null { + // Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + const valueMatch = (value as string).match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/); + if (valueMatch === null) { + // TODO: Display something if value is not valid + return null; + } + const [r, g, b, a] = valueMatch.splice(1, 4).map(v => Number(v)); + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) + ((1 << 8) + Math.floor((1-a)*255)).toString(16).slice(1); + }, valueChanged (value: string | number | boolean | Date | null) { if (value instanceof Date) { value = value.toISOString(); } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && value !== null && value.toString().charAt(0) !== '#') { + const newValue = this.rgbaToHex(value as string); + if (newValue !== null) { + this.tempValue = newValue; + value = newValue; + } + } + const parameterData = { node: this.node !== null ? this.node.name : this.nodeName, name: this.path, @@ -570,6 +607,13 @@ export default mixins( this.nodeName = this.node.name; } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && this.displayValue !== null && this.displayValue.toString().charAt(0) !== '#') { + const newValue = this.rgbaToHex(this.displayValue as string); + if (newValue !== null) { + this.tempValue = newValue; + } + } + if (this.remoteMethod !== undefined && this.node !== null) { // Make sure to load the parameter options // directly and whenever the credentials change diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 9e10000ffd..5c3b9c53aa 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -9,6 +9,12 @@ import { INodeTypeDescription, } from 'n8n-workflow'; import * as gm from 'gm'; +import { file } from 'tmp-promise'; +import { + writeFile as fsWriteFile, +} from 'fs'; +import { promisify } from 'util'; +const fsWriteFileAsync = promisify(fsWriteFile); export class EditImage implements INodeType { @@ -61,6 +67,11 @@ export class EditImage implements INodeType { value: 'resize', description: 'Change the size of image', }, + { + name: 'Shear', + value: 'shear', + description: 'Shear image along the X or Y axis', + }, { name: 'Text', value: 'text', @@ -385,6 +396,11 @@ export class EditImage implements INodeType { value: 'onlyIfSmaller', description: 'Resize only if image is smaller than width or height', }, + { + name: 'Percent', + value: 'percent', + description: 'Width and height are specified in percents.', + }, ], default: 'maximumArea', displayOptions: { @@ -422,7 +438,10 @@ export class EditImage implements INodeType { displayName: 'Background Color', name: 'backgroundColor', type: 'color', - default: '#ffffff', + default: '#ffffffff', + typeOptions: { + showAlpha: true, + }, displayOptions: { show: { operation: [ @@ -433,6 +452,39 @@ export class EditImage implements INodeType { description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..', }, + + // ---------------------------------- + // shear + // ---------------------------------- + { + displayName: 'Degrees X', + name: 'degreesX', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'shear', + ], + }, + }, + description: 'X (horizontal) shear degrees.', + }, + { + displayName: 'Degrees Y', + name: 'degreesY', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'shear', + ], + }, + }, + description: 'Y (vertical) shear degrees.', + }, + { displayName: 'Options', name: 'options', @@ -503,7 +555,6 @@ export class EditImage implements INodeType { }, description: 'Sets the jpeg|png|tiff compression level from 0 to 100 (best).', }, - ], }, ], @@ -529,6 +580,8 @@ export class EditImage implements INodeType { let gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); + gmInstance = gmInstance.background('transparent'); + if (operation === 'blur') { const blur = this.getNodeParameter('blur') as number; const sigma = this.getNodeParameter('sigma') as number; @@ -574,6 +627,8 @@ export class EditImage implements INodeType { option = '<'; } else if (resizeOption === 'onlyIfLarger') { option = '>'; + } else if (resizeOption === 'percent') { + option = '%'; } gmInstance = gmInstance.resize(width, height, option); @@ -581,6 +636,10 @@ export class EditImage implements INodeType { const rotate = this.getNodeParameter('rotate') as number; const backgroundColor = this.getNodeParameter('backgroundColor') as string; gmInstance = gmInstance.rotate(backgroundColor, rotate); + } else if (operation === 'shear') { + const xDegrees = this.getNodeParameter('degreesX') as number; + const yDegress = this.getNodeParameter('degreesY') as number; + gmInstance = gmInstance.shear(xDegrees, yDegress); } else if (operation === 'text') { const fontColor = this.getNodeParameter('fontColor') as string; const fontSize = this.getNodeParameter('fontSize') as number; @@ -624,6 +683,8 @@ export class EditImage implements INodeType { // data references which do not get changed still stay behind // but the incoming data does not get changed. Object.assign(newItem.binary, item.binary); + // Make a deep copy of the binary data we change + newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); } if (options.quality !== undefined) { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index bf0fc63e13..c6e42e0d28 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -408,6 +408,7 @@ export interface INodePropertyTypeOptions { numberStepSize?: number; // Supported by: number password?: boolean; // Supported by: string rows?: number; // Supported by: string + showAlpha?: boolean; // Supported by: color [key: string]: boolean | number | string | EditorTypes | undefined | string[]; }