From 7826cc06e99b64c0ed657bbdc7517d9d2f7396c3 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 25 Dec 2020 23:03:12 +0100 Subject: [PATCH] :zap: Add "Multi Step" option to EditImage Node --- packages/nodes-base/nodes/EditImage.node.ts | 1696 ++++++++++--------- 1 file changed, 910 insertions(+), 786 deletions(-) diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index f11aee8737..d5366bda9e 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -6,6 +6,7 @@ import { IDataObject, ILoadOptionsFunctions, INodeExecutionData, + INodeProperties, INodePropertyOptions, INodeType, INodeTypeDescription, @@ -17,12 +18,701 @@ import { } from 'path'; import { writeFile as fsWriteFile, + writeFileSync as fsWriteFileSync, } from 'fs'; import { promisify } from 'util'; const fsWriteFileAsync = promisify(fsWriteFile); import * as getSystemFonts from 'get-system-fonts'; +const nodeOperations: INodePropertyOptions[] = [ + { + name: 'Blur', + value: 'blur', + description: 'Adds a blur to the image and so makes it less sharp', + }, + { + name: 'Border', + value: 'border', + description: 'Adds a border to the image', + }, + { + name: 'Composite', + value: 'composite', + description: 'Composite image on top of another one', + }, + { + name: 'Create', + value: 'create', + description: 'Create a new image', + }, + { + name: 'Crop', + value: 'crop', + description: 'Crops the image', + }, + { + name: 'Draw', + value: 'draw', + description: 'Draw on image', + }, + { + name: 'Rotate', + value: 'rotate', + description: 'Rotate image', + }, + { + name: 'Resize', + 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', + description: 'Adds text to image', + }, +]; + + +const nodeOperationOptions: INodeProperties[] = [ + + // ---------------------------------- + // create + // ---------------------------------- + { + displayName: 'Background Color', + name: 'backgroundColor', + type: 'color', + default: '#ffffff00', + typeOptions: { + showAlpha: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + }, + }, + description: 'The background color of the image to create.', + }, + { + displayName: 'Image Width', + name: 'width', + type: 'number', + default: 50, + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + }, + }, + description: 'The width of the image to create.', + }, + { + displayName: 'Image Height', + name: 'height', + type: 'number', + default: 50, + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + }, + }, + description: 'The height of the image to create.', + }, + + + // ---------------------------------- + // draw + // ---------------------------------- + { + displayName: 'Primitive', + name: 'primitive', + type: 'options', + displayOptions: { + show: { + operation: [ + 'draw', + ], + }, + }, + options: [ + { + name: 'Line', + value: 'line', + }, + { + name: 'Rectangle', + value: 'rectangle', + }, + ], + default: 'rectangle', + description: 'The primitive to draw.', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#ff000000', + typeOptions: { + showAlpha: true, + }, + displayOptions: { + show: { + operation: [ + 'draw', + ], + }, + }, + description: 'The color of the primitive to draw', + }, + { + displayName: 'Start Position X', + name: 'startPositionX', + type: 'number', + default: 50, + displayOptions: { + show: { + operation: [ + 'draw', + ], + primitive: [ + 'line', + 'rectangle', + ], + }, + }, + description: 'X (horizontal) start position of the primitive.', + }, + { + displayName: 'Start Position Y', + name: 'startPositionY', + type: 'number', + default: 50, + displayOptions: { + show: { + operation: [ + 'draw', + ], + primitive: [ + 'line', + 'rectangle', + ], + }, + }, + description: 'Y (horizontal) start position of the primitive.', + }, + { + displayName: 'End Position X', + name: 'endPositionX', + type: 'number', + default: 250, + displayOptions: { + show: { + operation: [ + 'draw', + ], + primitive: [ + 'line', + 'rectangle', + ], + }, + }, + description: 'X (horizontal) end position of the primitive.', + }, + { + displayName: 'End Position Y', + name: 'endPositionY', + type: 'number', + default: 250, + displayOptions: { + show: { + operation: [ + 'draw', + ], + primitive: [ + 'line', + 'rectangle', + ], + }, + }, + description: 'Y (horizontal) end position of the primitive.', + }, + { + displayName: 'Corner Radius', + name: 'cornerRadius', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'draw', + ], + primitive: [ + 'rectangle', + ], + }, + }, + description: 'The radius of the corner to create round corners.', + }, + + // ---------------------------------- + // text + // ---------------------------------- + { + displayName: 'Text', + name: 'text', + typeOptions: { + rows: 5, + }, + type: 'string', + default: '', + placeholder: 'Text to render', + displayOptions: { + show: { + operation: [ + 'text', + ], + }, + }, + description: 'Text to write on the image.', + }, + { + displayName: 'Font Size', + name: 'fontSize', + type: 'number', + default: 18, + displayOptions: { + show: { + operation: [ + 'text', + ], + }, + }, + description: 'Size of the text.', + }, + { + displayName: 'Font Color', + name: 'fontColor', + type: 'color', + default: '#000000', + displayOptions: { + show: { + operation: [ + 'text', + ], + }, + }, + description: 'Color of the text.', + }, + { + displayName: 'Position X', + name: 'positionX', + type: 'number', + default: 50, + displayOptions: { + show: { + operation: [ + 'text', + ], + }, + }, + description: 'X (horizontal) position of the text.', + }, + { + displayName: 'Position Y', + name: 'positionY', + type: 'number', + default: 50, + displayOptions: { + show: { + operation: [ + 'text', + ], + }, + }, + description: 'Y (vertical) position of the text.', + }, + { + displayName: 'Max Line Length', + name: 'lineLength', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 80, + displayOptions: { + show: { + operation: [ + 'text', + ], + }, + }, + description: 'Max amount of characters in a line before a
line-break should get added.', + }, + + // ---------------------------------- + // blur + // ---------------------------------- + { + displayName: 'Blur', + name: 'blur', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 1000, + }, + default: 5, + displayOptions: { + show: { + operation: [ + 'blur', + ], + }, + }, + description: 'How strong the blur should be', + }, + { + displayName: 'Sigma', + name: 'sigma', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 1000, + }, + default: 2, + displayOptions: { + show: { + operation: [ + 'blur', + ], + }, + }, + description: 'The sigma of the blur', + }, + + + // ---------------------------------- + // border + // ---------------------------------- + { + displayName: 'Border Width', + name: 'borderWidth', + type: 'number', + default: 10, + displayOptions: { + show: { + operation: [ + 'border', + ], + }, + }, + description: 'The width of the border', + }, + { + displayName: 'Border Height', + name: 'borderHeight', + type: 'number', + default: 10, + displayOptions: { + show: { + operation: [ + 'border', + ], + }, + }, + description: 'The height of the border', + }, + { + displayName: 'Border Color', + name: 'borderColor', + type: 'color', + default: '#000000', + displayOptions: { + show: { + operation: [ + 'border', + ], + }, + }, + description: 'Color of the border.', + }, + + + // ---------------------------------- + // composite + // ---------------------------------- + { + displayName: 'Composite Image Property', + name: 'dataPropertyNameComposite', + type: 'string', + default: '', + placeholder: 'data2', + displayOptions: { + show: { + operation: [ + 'composite', + ], + }, + }, + description: 'The name of the binary property which contains the data of the image to
composite on top of image which is found in Property Name.', + }, + { + displayName: 'Position X', + name: 'positionX', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'composite', + ], + }, + }, + description: 'X (horizontal) position of composite image.', + }, + { + displayName: 'Position Y', + name: 'positionY', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'composite', + ], + }, + }, + description: 'Y (vertical) position of composite image.', + }, + + // ---------------------------------- + // crop + // ---------------------------------- + { + displayName: 'Width', + name: 'width', + type: 'number', + default: 500, + displayOptions: { + show: { + operation: [ + 'crop', + ], + }, + }, + description: 'Crop width', + }, + { + displayName: 'Height', + name: 'height', + type: 'number', + default: 500, + displayOptions: { + show: { + operation: [ + 'crop', + ], + }, + }, + description: 'Crop height', + }, + { + displayName: 'Position X', + name: 'positionX', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'crop', + ], + }, + }, + description: 'X (horizontal) position to crop from.', + }, + { + displayName: 'Position Y', + name: 'positionY', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'crop', + ], + }, + }, + description: 'Y (vertical) position to crop from.', + }, + + // ---------------------------------- + // resize + // ---------------------------------- + { + displayName: 'Width', + name: 'width', + type: 'number', + default: 500, + displayOptions: { + show: { + operation: [ + 'resize', + ], + }, + }, + description: 'New width of the image', + }, + { + displayName: 'Height', + name: 'height', + type: 'number', + default: 500, + displayOptions: { + show: { + operation: [ + 'resize', + ], + }, + }, + description: 'New height of the image', + }, + { + displayName: 'Option', + name: 'resizeOption', + type: 'options', + options: [ + { + name: 'Ignore Aspect Ratio', + value: 'ignoreAspectRatio', + description: 'Ignore aspect ratio and resize exactly to specified values', + }, + { + name: 'Maximum area', + value: 'maximumArea', + description: 'Specified values are maximum area', + }, + { + name: 'Minimum Area', + value: 'minimumArea', + description: 'Specified values are minimum area', + }, + { + name: 'Only if larger', + value: 'onlyIfLarger', + description: 'Resize only if image is larger than width or height', + }, + { + name: 'Only if smaller', + 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: { + show: { + operation: [ + 'resize', + ], + }, + }, + description: 'How to resize the image.', + }, + + // ---------------------------------- + // rotate + // ---------------------------------- + { + displayName: 'Rotate', + name: 'rotate', + type: 'number', + typeOptions: { + minValue: -360, + maxValue: 360, + }, + default: 0, + displayOptions: { + show: { + operation: [ + 'rotate', + ], + }, + }, + description: 'How much the image should be rotated', + }, + { + displayName: 'Background Color', + name: 'backgroundColor', + type: 'color', + default: '#ffffffff', + typeOptions: { + showAlpha: true, + }, + displayOptions: { + show: { + operation: [ + 'rotate', + ], + }, + }, + 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.', + }, +]; + + export class EditImage implements INodeType { description: INodeTypeDescription = { displayName: 'Edit Image', @@ -43,66 +733,25 @@ export class EditImage implements INodeType { name: 'operation', type: 'options', options: [ - { - name: 'Blur', - value: 'blur', - description: 'Adds a blur to the image and so makes it less sharp', - }, - { - name: 'Border', - value: 'border', - description: 'Adds a border to the image', - }, - { - name: 'Create', - value: 'create', - description: 'Create a new image', - }, - { - name: 'Crop', - value: 'crop', - description: 'Crops the image', - }, - { - name: 'Composite', - value: 'composite', - description: 'Composite image on top of another one', - }, - { - name: 'Draw', - value: 'draw', - description: 'Draw on image', - }, { name: 'Get Information', value: 'information', description: 'Returns image information like resolution', }, { - name: 'Rotate', - value: 'rotate', - description: 'Rotate image', + name: 'Multi Step', + value: 'multiStep', + description: 'Perform multiple operations', }, - { - name: 'Resize', - 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', - description: 'Adds text to image', - }, - ], + ...nodeOperations, + ].sort((a, b) => { + if ((a as INodePropertyOptions).name.toLowerCase() < (b as INodePropertyOptions).name.toLowerCase()) { return -1; } + if ((a as INodePropertyOptions).name.toLowerCase() > (b as INodePropertyOptions).name.toLowerCase()) { return 1; } + return 0; + }) as INodePropertyOptions[], default: 'border', description: 'The operation to perform.', }, - { displayName: 'Property Name', name: 'dataPropertyName', @@ -113,636 +762,62 @@ export class EditImage implements INodeType { // ---------------------------------- - // create + // multiStep // ---------------------------------- { - displayName: 'Background Color', - name: 'backgroundColor', - type: 'color', - default: '#ffffff00', + displayName: 'Operations', + name: 'operations', + placeholder: 'Add Operation', + type: 'fixedCollection', typeOptions: { - showAlpha: true, + multipleValues: true, }, displayOptions: { show: { operation: [ - 'create', - ], - }, - }, - description: 'The background color of the image to create.', - }, - { - displayName: 'Image Width', - name: 'width', - type: 'number', - default: 50, - typeOptions: { - minValue: 1, - }, - displayOptions: { - show: { - operation: [ - 'create', - ], - }, - }, - description: 'The width of the image to create.', - }, - { - displayName: 'Image Height', - name: 'height', - type: 'number', - default: 50, - typeOptions: { - minValue: 1, - }, - displayOptions: { - show: { - operation: [ - 'create', - ], - }, - }, - description: 'The height of the image to create.', - }, - - - // ---------------------------------- - // draw - // ---------------------------------- - { - displayName: 'Primitive', - name: 'primitive', - type: 'options', - displayOptions: { - show: { - operation: [ - 'draw', + 'multiStep', ], }, }, + description: 'The operations to perform.', + default: {}, options: [ { - name: 'Line', - value: 'line', - }, - { - name: 'Rectangle', - value: 'rectangle', + name: 'operations', + displayName: 'Operations', + values: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: nodeOperations, + default: '', + description: 'The operation to perform.', + }, + ...nodeOperationOptions, + { + displayName: 'Font', + name: 'font', + type: 'options', + displayOptions: { + show: { + 'operation': [ + 'text', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getFonts', + }, + default: 'default', + description: 'The font to use.', + }, + ], }, ], - default: 'rectangle', - description: 'The primitive to draw.', - }, - { - displayName: 'Color', - name: 'color', - type: 'color', - default: '#ff000000', - typeOptions: { - showAlpha: true, - }, - displayOptions: { - show: { - operation: [ - 'draw', - ], - }, - }, - description: 'The color of the primitive to draw', - }, - { - displayName: 'Start Position X', - name: 'startPositionX', - type: 'number', - default: 50, - displayOptions: { - show: { - operation: [ - 'draw', - ], - primitive: [ - 'line', - 'rectangle', - ], - }, - }, - description: 'X (horizontal) start position of the primitive.', - }, - { - displayName: 'Start Position Y', - name: 'startPositionY', - type: 'number', - default: 50, - displayOptions: { - show: { - operation: [ - 'draw', - ], - primitive: [ - 'line', - 'rectangle', - ], - }, - }, - description: 'Y (horizontal) start position of the primitive.', - }, - { - displayName: 'End Position X', - name: 'endPositionX', - type: 'number', - default: 250, - displayOptions: { - show: { - operation: [ - 'draw', - ], - primitive: [ - 'line', - 'rectangle', - ], - }, - }, - description: 'X (horizontal) end position of the primitive.', - }, - { - displayName: 'End Position Y', - name: 'endPositionY', - type: 'number', - default: 250, - displayOptions: { - show: { - operation: [ - 'draw', - ], - primitive: [ - 'line', - 'rectangle', - ], - }, - }, - description: 'Y (horizontal) end position of the primitive.', - }, - { - displayName: 'Corner Radius', - name: 'cornerRadius', - type: 'number', - default: 0, - displayOptions: { - show: { - operation: [ - 'draw', - ], - primitive: [ - 'rectangle', - ], - }, - }, - description: 'The radius of the corner to create round corners.', - }, - - // ---------------------------------- - // text - // ---------------------------------- - { - displayName: 'Text', - name: 'text', - typeOptions: { - rows: 5, - }, - type: 'string', - default: '', - placeholder: 'Text to render', - displayOptions: { - show: { - operation: [ - 'text', - ], - }, - }, - description: 'Text to write on the image.', - }, - { - displayName: 'Font Size', - name: 'fontSize', - type: 'number', - default: 18, - displayOptions: { - show: { - operation: [ - 'text', - ], - }, - }, - description: 'Size of the text.', - }, - { - displayName: 'Font Color', - name: 'fontColor', - type: 'color', - default: '#000000', - displayOptions: { - show: { - operation: [ - 'text', - ], - }, - }, - description: 'Color of the text.', - }, - { - displayName: 'Position X', - name: 'positionX', - type: 'number', - default: 50, - displayOptions: { - show: { - operation: [ - 'text', - ], - }, - }, - description: 'X (horizontal) position of the text.', - }, - { - displayName: 'Position Y', - name: 'positionY', - type: 'number', - default: 50, - displayOptions: { - show: { - operation: [ - 'text', - ], - }, - }, - description: 'Y (vertical) position of the text.', - }, - { - displayName: 'Max Line Length', - name: 'lineLength', - type: 'number', - typeOptions: { - minValue: 1, - }, - default: 80, - displayOptions: { - show: { - operation: [ - 'text', - ], - }, - }, - description: 'Max amount of characters in a line before a
line-break should get added.', - }, - - - // ---------------------------------- - // blur - // ---------------------------------- - { - displayName: 'Blur', - name: 'blur', - type: 'number', - typeOptions: { - minValue: 0, - maxValue: 1000, - }, - default: 5, - displayOptions: { - show: { - operation: [ - 'blur', - ], - }, - }, - description: 'How strong the blur should be', - }, - { - displayName: 'Sigma', - name: 'sigma', - type: 'number', - typeOptions: { - minValue: 0, - maxValue: 1000, - }, - default: 2, - displayOptions: { - show: { - operation: [ - 'blur', - ], - }, - }, - description: 'The sigma of the blur', - }, - - - // ---------------------------------- - // border - // ---------------------------------- - { - displayName: 'Border Width', - name: 'borderWidth', - type: 'number', - default: 10, - displayOptions: { - show: { - operation: [ - 'border', - ], - }, - }, - description: 'The width of the border', - }, - { - displayName: 'Border Height', - name: 'borderHeight', - type: 'number', - default: 10, - displayOptions: { - show: { - operation: [ - 'border', - ], - }, - }, - description: 'The height of the border', - }, - { - displayName: 'Border Color', - name: 'borderColor', - type: 'color', - default: '#000000', - displayOptions: { - show: { - operation: [ - 'border', - ], - }, - }, - description: 'Color of the border.', - }, - - - // ---------------------------------- - // composite - // ---------------------------------- - { - displayName: 'Composite Image Property', - name: 'dataPropertyNameComposite', - type: 'string', - default: '', - placeholder: 'data2', - displayOptions: { - show: { - operation: [ - 'composite', - ], - }, - }, - description: 'The name of the binary property which contains the data of the image to
composite on top of image which is found in Property Name.', - }, - { - displayName: 'Position X', - name: 'positionX', - type: 'number', - default: 0, - displayOptions: { - show: { - operation: [ - 'composite', - ], - }, - }, - description: 'X (horizontal) position of composite image.', - }, - { - displayName: 'Position Y', - name: 'positionY', - type: 'number', - default: 0, - displayOptions: { - show: { - operation: [ - 'composite', - ], - }, - }, - description: 'Y (vertical) position of composite image.', - }, - - // ---------------------------------- - // crop - // ---------------------------------- - { - displayName: 'Width', - name: 'width', - type: 'number', - default: 500, - displayOptions: { - show: { - operation: [ - 'crop', - ], - }, - }, - description: 'Crop width', - }, - { - displayName: 'Height', - name: 'height', - type: 'number', - default: 500, - displayOptions: { - show: { - operation: [ - 'crop', - ], - }, - }, - description: 'Crop height', - }, - { - displayName: 'Position X', - name: 'positionX', - type: 'number', - default: 0, - displayOptions: { - show: { - operation: [ - 'crop', - ], - }, - }, - description: 'X (horizontal) position to crop from.', - }, - { - displayName: 'Position Y', - name: 'positionY', - type: 'number', - default: 0, - displayOptions: { - show: { - operation: [ - 'crop', - ], - }, - }, - description: 'Y (vertical) position to crop from.', - }, - - // ---------------------------------- - // resize - // ---------------------------------- - { - displayName: 'Width', - name: 'width', - type: 'number', - default: 500, - displayOptions: { - show: { - operation: [ - 'resize', - ], - }, - }, - description: 'New width of the image', - }, - { - displayName: 'Height', - name: 'height', - type: 'number', - default: 500, - displayOptions: { - show: { - operation: [ - 'resize', - ], - }, - }, - description: 'New height of the image', - }, - { - displayName: 'Option', - name: 'resizeOption', - type: 'options', - options: [ - { - name: 'Ignore Aspect Ratio', - value: 'ignoreAspectRatio', - description: 'Ignore aspect ratio and resize exactly to specified values', - }, - { - name: 'Maximum area', - value: 'maximumArea', - description: 'Specified values are maximum area', - }, - { - name: 'Minimum Area', - value: 'minimumArea', - description: 'Specified values are minimum area', - }, - { - name: 'Only if larger', - value: 'onlyIfLarger', - description: 'Resize only if image is larger than width or height', - }, - { - name: 'Only if smaller', - 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: { - show: { - operation: [ - 'resize', - ], - }, - }, - description: 'How to resize the image.', - }, - - // ---------------------------------- - // rotate - // ---------------------------------- - { - displayName: 'Rotate', - name: 'rotate', - type: 'number', - typeOptions: { - minValue: -360, - maxValue: 360, - }, - default: 0, - displayOptions: { - show: { - operation: [ - 'rotate', - ], - }, - }, - description: 'How much the image should be rotated', - }, - { - displayName: 'Background Color', - name: 'backgroundColor', - type: 'color', - default: '#ffffffff', - typeOptions: { - showAlpha: true, - }, - displayOptions: { - show: { - operation: [ - 'rotate', - ], - }, - }, - 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.', }, + ...nodeOperationOptions, { displayName: 'Options', name: 'options', @@ -882,17 +957,91 @@ export class EditImage implements INodeType { const options = this.getNodeParameter('options', {}) as IDataObject; - let gmInstance: gm.State; - if (operation === 'create') { - const backgroundColor = this.getNodeParameter('backgroundColor') as string; - const width = this.getNodeParameter('width') as number; - const height = this.getNodeParameter('height') as number; + const cleanupFunctions: Array<() => void> = []; - gmInstance = gm(width, height, backgroundColor); - if (!options.format) { - options.format = 'png'; - } + let gmInstance: gm.State; + + const requiredOperationParameters: { + [key: string]: string[], + } = { + blur: [ + 'blur', + 'sigma', + ], + create: [ + 'backgroundColor', + 'height', + 'width', + ], + crop: [ + 'height', + 'positionX', + 'positionY', + 'width', + ], + composite: [ + 'dataPropertyNameComposite', + 'positionX', + 'positionY', + ], + draw: [ + 'color', + 'cornerRadius', + 'endPositionX', + 'endPositionY', + 'primitive', + 'startPositionX', + 'startPositionY', + ], + information: [], + resize: [ + 'height', + 'resizeOption', + 'width', + ], + rotate: [ + 'backgroundColor', + 'rotate', + ], + shear: [ + 'degreesX', + 'degreesY', + ], + text: [ + 'font', + 'fontColor', + 'fontSize', + 'lineLength', + 'positionX', + 'positionY', + 'text', + ], + }; + + let operations: IDataObject[] = []; + if (operation === 'multiStep') { + // Operation parameters are already in the correct format + const operationsData = this.getNodeParameter('operations', { operations: [] }) as IDataObject; + operations = operationsData.operations as IDataObject[]; } else { + // Operation parameters have to first get collected + const operationParameters: IDataObject = {}; + requiredOperationParameters[operation].forEach(parameterName => { + try { + operationParameters[parameterName] = this.getNodeParameter(parameterName); + } catch (e) {} + }); + + operations = [ + { + operation, + ...operationParameters, + } + ]; + } + + if (operations[0].operation !== 'create') { + // "create" generates a new image so does not require any incoming data. if (item.binary === undefined) { throw new Error('Item does not contain any binary data.'); } @@ -905,60 +1054,8 @@ export class EditImage implements INodeType { gmInstance = gmInstance.background('transparent'); } - const cleanupFunctions: Array<() => void> = []; - - if (operation === 'blur') { - const blur = this.getNodeParameter('blur') as number; - const sigma = this.getNodeParameter('sigma') as number; - gmInstance = gmInstance.blur(blur, sigma); - } else if (operation === 'border') { - const borderWidth = this.getNodeParameter('borderWidth') as number; - const borderHeight = this.getNodeParameter('borderHeight') as number; - const borderColor = this.getNodeParameter('borderColor') as string; - - gmInstance = gmInstance.borderColor(borderColor).border(borderWidth, borderHeight); - } else if (operation === 'composite') { - const dataPropertyNameComposite = this.getNodeParameter('dataPropertyNameComposite') as string; - const positionX = this.getNodeParameter('positionX') as number; - const positionY = this.getNodeParameter('positionY') as number; - - const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; - - if (item.binary![dataPropertyNameComposite as string] === undefined) { - throw new Error(`Item does not contain any binary data with the name "${dataPropertyNameComposite}".`); - } - - const { fd, path, cleanup } = await file(); - cleanupFunctions.push(cleanup); - fsWriteFileAsync(fd, Buffer.from(item.binary![dataPropertyNameComposite as string].data, BINARY_ENCODING)); - - gmInstance = gmInstance.composite(path).geometry(geometryString); - } else if (operation === 'crop') { - const width = this.getNodeParameter('width') as number; - const height = this.getNodeParameter('height') as number; - - const positionX = this.getNodeParameter('positionX') as number; - const positionY = this.getNodeParameter('positionY') as number; - - gmInstance = gmInstance.crop(width, height, positionX, positionY); - } else if (operation === 'draw') { - const startPositionX = this.getNodeParameter('startPositionX') as number; - const startPositionY = this.getNodeParameter('startPositionY') as number; - const endPositionX = this.getNodeParameter('endPositionX') as number; - const endPositionY = this.getNodeParameter('endPositionY') as number; - const primitive = this.getNodeParameter('primitive') as string; - const color = this.getNodeParameter('color') as string; - - gmInstance = gmInstance.fill(color); - - if (primitive === 'line') { - gmInstance = gmInstance.drawLine(startPositionX, startPositionY, endPositionX, endPositionY); - } else if (primitive === 'rectangle') { - const cornerRadius = this.getNodeParameter('cornerRadius') as number; - gmInstance = gmInstance.drawRectangle(startPositionX, startPositionY, endPositionX, endPositionY, cornerRadius || undefined); - } - - } else if (operation === 'information') { + if (operation === 'information') { + // Just return the information const imageData = await new Promise((resolve, reject) => { gmInstance = gmInstance.identify((error, imageData) => { if (error) { @@ -970,72 +1067,99 @@ export class EditImage implements INodeType { }); item.json = imageData; - } else if (operation === 'resize') { - const width = this.getNodeParameter('width') as number; - const height = this.getNodeParameter('height') as number; - const resizeOption = this.getNodeParameter('resizeOption') as string; + return item; + } - // By default use "maximumArea" - let option: gm.ResizeOption = '@'; - if (resizeOption === 'ignoreAspectRatio') { - option = '!'; - } else if (resizeOption === 'minimumArea') { - option = '^'; - } else if (resizeOption === 'onlyIfSmaller') { - option = '<'; - } else if (resizeOption === 'onlyIfLarger') { - option = '>'; - } else if (resizeOption === 'percent') { - option = '%'; - } + for (const operationData of operations) { + if (operationData.operation === 'blur') { + gmInstance = gmInstance!.blur(operationData.blur as number, operationData.sigma as number); + } else if (operationData.operation === 'border') { + gmInstance = gmInstance!.borderColor(operationData.borderColor as string).border(operationData.borderWidth as number, operationData.borderHeight as number); + } else if (operationData.operation === 'composite') { + const positionX = operationData.positionX as number; + const positionY = operationData.positionY as number; - gmInstance = gmInstance.resize(width, height, option); - } else if (operation === 'rotate') { - 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; - const lineLength = this.getNodeParameter('lineLength') as number; - const positionX = this.getNodeParameter('positionX') as number; - const positionY = this.getNodeParameter('positionY') as number; - const text = this.getNodeParameter('text') as string || ''; + const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; - // Split the text in multiple lines - const lines: string[] = []; - let currentLine = ''; - (text as string).split('\n').forEach((textLine: string) => { - textLine.split(' ').forEach((textPart: string) => { - if ((currentLine.length + textPart.length + 1) > lineLength) { - lines.push(currentLine.trim()); - currentLine = `${textPart} `; - return; + if (item.binary![operationData.dataPropertyNameComposite as string] === undefined) { + throw new Error(`Item does not contain any binary data with the name "${operationData.dataPropertyNameComposite}".`); + } + + gmInstance = await gmInstance!; + const { fd, path, cleanup } = await file(); + cleanupFunctions.push(cleanup); + await fsWriteFileAsync(fd, Buffer.from(item.binary![operationData.dataPropertyNameComposite as string].data, BINARY_ENCODING)); + + gmInstance = gmInstance!.composite(path).geometry(geometryString); + } else if (operationData.operation === 'create') { + gmInstance = gm(operationData.width as number, operationData.height as number, operationData.backgroundColor as string); + if (!options.format) { + options.format = 'png'; } - currentLine += `${textPart} `; + } else if (operationData.operation === 'crop') { + gmInstance = gmInstance!.crop(operationData.width as number, operationData.height as number, operationData.positionX as number, operationData.positionY as number); + } else if (operationData.operation === 'draw') { + gmInstance = gmInstance!.fill(operationData.color as string); + + if (operationData.primitive === 'line') { + gmInstance = gmInstance.drawLine(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number); + } else if (operationData.primitive === 'rectangle') { + gmInstance = gmInstance.drawRectangle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number, operationData.cornerRadius as number || undefined); + } + } else if (operationData.operation === 'resize') { + const resizeOption = operationData.resizeOption as string; + + // By default use "maximumArea" + let option: gm.ResizeOption = '@'; + if (resizeOption === 'ignoreAspectRatio') { + option = '!'; + } else if (resizeOption === 'minimumArea') { + option = '^'; + } else if (resizeOption === 'onlyIfSmaller') { + option = '<'; + } else if (resizeOption === 'onlyIfLarger') { + option = '>'; + } else if (resizeOption === 'percent') { + option = '%'; + } + + gmInstance = gmInstance!.resize(operationData.width as number, operationData.height as number, option); + } else if (operationData.operation === 'rotate') { + gmInstance = gmInstance!.rotate(operationData.backgroundColor as string, operationData.rotate as number); + } else if (operationData.operation === 'shear') { + gmInstance = gmInstance!.shear(operationData.degreesX as number, operationData.degreesY as number); + } else if (operationData.operation === 'text') { + // Split the text in multiple lines + const lines: string[] = []; + let currentLine = ''; + (operationData.text as string).split('\n').forEach((textLine: string) => { + textLine.split(' ').forEach((textPart: string) => { + if ((currentLine.length + textPart.length + 1) > (operationData.lineLength as number)) { + lines.push(currentLine.trim()); + currentLine = `${textPart} `; + return; + } + currentLine += `${textPart} `; + }); + + lines.push(currentLine.trim()); + currentLine = ''; }); - lines.push(currentLine.trim()); - currentLine = ''; - }); + // Combine the lines to a single string + const renderText = lines.join('\n'); - // Combine the lines to a single string - const renderText = lines.join('\n'); + const font = options.font || operationData.font; - if (options.font && options.font !== 'default') { - gmInstance = gmInstance.font(options.font as string); + if (font && font !== 'default') { + gmInstance = gmInstance!.font(font as string); + } + + gmInstance = gmInstance! + .fill(operationData.fontColor as string) + .fontSize(operationData.fontSize as number) + .drawText(operationData.positionX as number, operationData.positionY as number, renderText); } - - gmInstance = gmInstance - .fill(fontColor) - .fontSize(fontSize) - .drawText(positionX, positionY, renderText); - } else if (operation !== 'create') { - throw new Error(`The operation "${operation}" is not supported!`); } const newItem: INodeExecutionData = { @@ -1060,11 +1184,11 @@ export class EditImage implements INodeType { } if (options.quality !== undefined) { - gmInstance = gmInstance.quality(options.quality as number); + gmInstance = gmInstance!.quality(options.quality as number); } if (options.format !== undefined) { - gmInstance = gmInstance.setFormat(options.format as string); + gmInstance = gmInstance!.setFormat(options.format as string); newItem.binary![dataPropertyName as string].fileExtension = options.format as string; newItem.binary![dataPropertyName as string].mimeType = `image/${options.format}`; const fileName = newItem.binary![dataPropertyName as string].fileName;