Finished off first iteration of image nodes

This commit is contained in:
John Andrews
2022-05-06 17:09:16 +12:00
parent b61c7b8c4d
commit 1e18ee8f01
39 changed files with 431 additions and 30 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,4 +3,11 @@
internal static class ExtensionMethods
{
public static string? EmptyAsNull(this string str) => str == string.Empty ? null : str;
public static void AddOrUpdate(this Dictionary<string, object> dict, string key, object value)
{
if (dict.ContainsKey(key))
dict[key] = value;
else
dict.Add(key, value);
}
}

10
ImageNodes/ImageInfo.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace FileFlows.ImageNodes;
public class ImageInfo
{
public int Width { get; set; }
public int Height { get; set; }
public string Format { get; set; }
public bool IsPortrait => Width < Height;
public bool IsLandscape => Height < Width;
}

Binary file not shown.

View File

@@ -13,6 +13,12 @@
"Vertical-Help": "If set the image will be flipped vertically, otherwise horizontally"
}
},
"ImageFile": {
"Outputs": {
"1": "Image file"
},
"Description": "An image file"
},
"ImageFormat": {
"Outputs": {
"1": "Image converted, saved to new temporary file"
@@ -23,6 +29,20 @@
"Format-Help": "The image format to convert to"
}
},
"ImageIsLandscape": {
"Outputs": {
"1": "Image is landscape",
"2": "Image is not landscape"
},
"Description": "Test if an image is landscape"
},
"ImageIsPortrait": {
"Outputs": {
"1": "Image is portrait",
"2": "Image is not portrait"
},
"Description": "Test if an image is portrait"
},
"ImageResizer": {
"Outputs": {
"1": "Image resized, saved to new temporary file"
@@ -32,11 +52,11 @@
"Format": "Format",
"Format-Help": "The image format to convert to",
"Width": "Width",
"Width-Suffix": "px",
"Height": "Height",
"Height-Suffix": "px",
"Mode": "Mode",
"Mode-Help": "The mode to use when resizing the image"
"Mode-Help": "The mode to use when resizing the image",
"Percent": "Percent",
"Percent-Help": "When selected, the width and height values become percentages, with 100 being 100%"
}
},
"ImageRotate": {

View File

@@ -0,0 +1,72 @@
using SixLabors.ImageSharp.Formats;
namespace FileFlows.ImageNodes.Images;
public abstract class ImageBaseNode:Node
{
private const string IMAGE_INFO = "ImageInfo";
protected void UpdateImageInfo(NodeParameters args, Dictionary<string, object> variables = null)
{
using var image = Image.Load(args.WorkingFile, out IImageFormat format);
var imageInfo = new ImageInfo
{
Width = image.Width,
Height = image.Height,
Format = format.Name
};
variables ??= new Dictionary<string, object>();
if (args.Parameters.ContainsKey(IMAGE_INFO))
args.Parameters[IMAGE_INFO] = imageInfo;
else
args.Parameters.Add(IMAGE_INFO, imageInfo);
variables.AddOrUpdate("img.Width", imageInfo.Width);
variables.AddOrUpdate("img.Height", imageInfo.Height);
variables.AddOrUpdate("img.Format", imageInfo.Format);
variables.AddOrUpdate("img.IsPortrait", imageInfo.IsPortrait);
variables.AddOrUpdate("img.IsLandscape", imageInfo.IsLandscape);
args.UpdateVariables(variables);
}
protected void UpdateImageInfo(NodeParameters args, Image image, IImageFormat format, Dictionary<string, object> variables = null)
{
var imageInfo = new ImageInfo
{
Width = image.Width,
Height = image.Height,
Format = format.Name
};
variables ??= new Dictionary<string, object>();
if (args.Parameters.ContainsKey(IMAGE_INFO))
args.Parameters[IMAGE_INFO] = imageInfo;
else
args.Parameters.Add(IMAGE_INFO, imageInfo);
variables.AddOrUpdate("img.Width", imageInfo.Width);
variables.AddOrUpdate("img.Height", imageInfo.Height);
variables.AddOrUpdate("img.Format", imageInfo.Format);
variables.AddOrUpdate("img.IsPortrait", imageInfo.IsPortrait);
variables.AddOrUpdate("img.IsLandscape", imageInfo.IsLandscape);
args.UpdateVariables(variables);
}
internal ImageInfo? GetImageInfo(NodeParameters args)
{
if (args.Parameters.ContainsKey(IMAGE_INFO) == false)
{
args.Logger?.WLog("No codec information loaded, use a 'Image File' node first");
return null;
}
var result = args.Parameters[IMAGE_INFO] as ImageInfo;
if (result == null)
{
args.Logger?.WLog("ImageInfo not found for file");
return null;
}
return result;
}
}

View File

@@ -0,0 +1,40 @@
namespace FileFlows.ImageNodes.Images;
using FileFlows.Plugin;
public class ImageFile : ImageBaseNode
{
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Input;
public override string Icon => "fas fa-file-image";
private Dictionary<string, object> _Variables;
public override Dictionary<string, object> Variables => _Variables;
public ImageFile()
{
_Variables = new Dictionary<string, object>()
{
{ "img.Width", 1920 },
{ "img.Heigh", 1080 },
{ "img.Format", "PNG" },
{ "img.IsPortrait", true },
{ "img.IsLandscape", false }
};
}
public override int Execute(NodeParameters args)
{
try
{
UpdateImageInfo(args, this.Variables);
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing MusicFile: " + ex.Message);
return -1;
}
}
}

View File

@@ -19,8 +19,7 @@ public class ImageFlip: ImageNode
using var image = Image.Load(args.WorkingFile, out IImageFormat format);
image.Mutate(c => c.Flip(Vertical ? FlipMode.Vertical : FlipMode.Horizontal));
var formatOpts = GetFormat(args);
SaveImage(image, formatOpts.file, formatOpts.format ?? format);
args.SetWorkingFile(formatOpts.file);
SaveImage(args, image, formatOpts.file, formatOpts.format ?? format);
return 1;
}

View File

@@ -15,8 +15,7 @@ public class ImageFormat: ImageNode
using var image = Image.Load(args.WorkingFile, out IImageFormat format);
var formatOpts = GetFormat(args);
SaveImage(image, formatOpts.file, formatOpts.format ?? format);
args.SetWorkingFile(formatOpts.file);
SaveImage(args, image, formatOpts.file, formatOpts.format ?? format);
return 1;
}
}

View File

@@ -0,0 +1,18 @@
namespace FileFlows.ImageNodes.Images;
public class ImageIsLandscape: ImageBaseNode
{
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
public override string Icon => "fas fa-image";
public override int Execute(NodeParameters args)
{
var img = GetImageInfo(args);
if (img == null)
return -1;
return img.IsLandscape ? 1 : 2;
}
}

View File

@@ -0,0 +1,18 @@
namespace FileFlows.ImageNodes.Images;
public class ImageIsPortrait : ImageBaseNode
{
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
public override string Icon => "fas fa-portrait";
public override int Execute(NodeParameters args)
{
var img = GetImageInfo(args);
if (img == null)
return -1;
return img.IsPortrait ? 1 : 2;
}
}

View File

@@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Formats.Webp;
namespace FileFlows.ImageNodes.Images;
public abstract class ImageNode : Node
public abstract class ImageNode : ImageBaseNode
{
[Select(nameof(FormatOptions), 1)]
public string Format { get; set; }
@@ -24,6 +24,7 @@ public abstract class ImageNode : Node
{
_FormatOptions = new List<ListOption>
{
new ListOption { Value = "", Label = "Same as source"},
new ListOption { Value = IMAGE_FORMAT_BMP, Label = "Bitmap"},
new ListOption { Value = IMAGE_FORMAT_GIF, Label = "GIF"},
new ListOption { Value = IMAGE_FORMAT_JPEG, Label = "JPEG"},
@@ -85,9 +86,14 @@ public abstract class ImageNode : Node
return (format, newFile);
}
protected void SaveImage(Image img, string file, IImageFormat format)
protected void SaveImage(NodeParameters args, Image img, string file, IImageFormat format, bool updateWorkingFile = true)
{
using Stream outStream = new FileStream(file, FileMode.Create);
img.Save(outStream, format);
if (updateWorkingFile)
{
args.SetWorkingFile(file);
UpdateImageInfo(args, img, format, Variables);
}
}
}

View File

@@ -14,9 +14,9 @@ namespace FileFlows.ImageNodes.Images;
public class ImageResizer: ImageNode
{
public override int Inputs => 1;
public override int Outputs => 2;
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Process;
public override string Icon => "fas fa-image";
public override string Icon => "fas fa-expand";
[Select(nameof(ResizeModes), 2)]
@@ -42,12 +42,15 @@ public class ImageResizer: ImageNode
}
[NumberInt(3)]
[Range(0, int.MaxValue)]
[Range(1, int.MaxValue)]
public int Width { get; set; }
[NumberInt(4)]
[Range(0, int.MaxValue)]
[Range(1, int.MaxValue)]
public int Height { get; set; }
[Boolean(5)]
public bool Percent { get; set; }
public override int Execute(NodeParameters args)
{
using var image = Image.Load(args.WorkingFile, out IImageFormat format);
@@ -65,15 +68,22 @@ public class ImageResizer: ImageNode
}
var formatOpts = GetFormat(args);
float w = Width;
float h = Height;
if (Percent)
{
w = (int)(image.Width * (w / 100f));
h = (int)(image.Height * (h / 100f));
}
image.Mutate(c => c.Resize(new ResizeOptions()
{
Size = new Size(Width, Height),
Size = new Size((int)w, (int)h),
Mode = rzMode
}));
SaveImage(image, formatOpts.file, formatOpts.format ?? format);
args.SetWorkingFile(formatOpts.file);
SaveImage(args, image, formatOpts.file, formatOpts.format ?? format);
return 1;
}
}

View File

@@ -36,8 +36,7 @@ public class ImageRotate: ImageNode
using var image = Image.Load(args.WorkingFile, out IImageFormat format);
image.Mutate(c => c.Rotate(Angle));
var formatOpts = GetFormat(args);
SaveImage(image, formatOpts.file, formatOpts.format ?? format);
args.SetWorkingFile(formatOpts.file);
SaveImage(args, image, formatOpts.file, formatOpts.format ?? format);
return 1;
}

View File

@@ -2,31 +2,53 @@
using FileFlows.ImageNodes.Images;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Runtime.InteropServices;
namespace FileFlows.ImageNodes.Tests;
[TestClass]
public class ImageNodesTests
{
string TestImage1;
string TestImage2;
string TempDir;
public ImageNodesTests()
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
TestImage1 = @"D:\videos\pictures\image1.jpg";
TestImage2 = @"D:\videos\pictures\image2.png";
TempDir = @"D:\videos\temp";
}
else
{
TestImage1 = "/home/john/Pictures/fileflows.png";
TestImage2 = "/home/john/Pictures/36410427.png";
TempDir = "/home/john/src/temp/";
}
}
[TestMethod]
public void ImageNodes_Basic_ImageFormat()
{
var args = new NodeParameters("/home/john/Pictures/fileflows.png", new TestLogger(), false, string.Empty)
var args = new NodeParameters(TestImage1, new TestLogger(), false, string.Empty)
{
TempPath = "/home/john/src/temp/"
TempPath = TempDir
};
var node = new ImageFormat();
node.Format = IMAGE_FORMAT_GIF;
Assert.AreEqual(1, node.Execute(args));
}
[TestMethod]
public void ImageNodes_Basic_Resize()
{
var args = new NodeParameters("/home/john/Pictures/fileflows.png", new TestLogger(), false, string.Empty)
var args = new NodeParameters(TestImage1, new TestLogger(), false, string.Empty)
{
TempPath = "/home/john/src/temp/"
TempPath = TempDir
};
var node = new ImageResizer();
@@ -35,13 +57,38 @@ public class ImageNodesTests
node.Mode = ResizeMode.Fill;
Assert.AreEqual(1, node.Execute(args));
}
[TestMethod]
public void ImageNodes_Basic_Resize_Percent()
{
var args = new NodeParameters(TestImage1, new TestLogger(), false, string.Empty)
{
TempPath = TempDir
};
var imgFile = new ImageFile();
imgFile.Execute(args);
int width = imgFile.GetImageInfo(args).Width;
int height = imgFile.GetImageInfo(args).Height;
var node = new ImageResizer();
node.Width = 200;
node.Height = 50;
node.Percent = true;
node.Mode = ResizeMode.Fill;
Assert.AreEqual(1, node.Execute(args));
var img = node.GetImageInfo(args);
Assert.IsNotNull(img);
Assert.AreEqual(width * 2, img.Width);
Assert.AreEqual(height / 2, img.Height);
}
[TestMethod]
public void ImageNodes_Basic_Flip()
{
var args = new NodeParameters("/home/john/Pictures/36410427.png", new TestLogger(), false, string.Empty)
var args = new NodeParameters(TestImage2, new TestLogger(), false, string.Empty)
{
TempPath = "/home/john/src/temp/"
TempPath = TempDir
};
var node = new ImageFlip();
@@ -53,9 +100,9 @@ public class ImageNodesTests
[TestMethod]
public void ImageNodes_Basic_Rotate()
{
var args = new NodeParameters("/home/john/Pictures/36410427.png", new TestLogger(), false, string.Empty)
var args = new NodeParameters(TestImage2, new TestLogger(), false, string.Empty)
{
TempPath = "/home/john/src/temp/"
TempPath = TempDir
};
var node = new ImageRotate();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -31,5 +31,135 @@
<param name="part">The flow part</param>
<returns>an insstance of the plugin node</returns>
</member>
<member name="T:FileFlows.ServerShared.Services.IPluginService">
<summary>
Plugin Service interface
</summary>
</member>
<member name="M:FileFlows.ServerShared.Services.IPluginService.GetAll">
<summary>
Get all plugin infos
</summary>
<returns>all plugin infos</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.IPluginService.Update(FileFlows.Shared.Models.PluginInfo)">
<summary>
Updates plugin info
</summary>
<param name="pluginInfo">the plugin info</param>
<returns>the updated plugininfo</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.IPluginService.Download(FileFlows.Shared.Models.PluginInfo)">
<summary>
Download a plugin
</summary>
<param name="plugin">the plugin to download</param>
<returns>the byte data of the plugin</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.IPluginService.GetSettingsJson(System.String)">
<summary>
Gets the settings json for a plugin
</summary>
<param name="pluginPackageName">the name of the plugin package</param>
<returns>the settings json</returns>
</member>
<member name="T:FileFlows.ServerShared.Services.PluginService">
<summary>
Plugin service
</summary>
</member>
<member name="M:FileFlows.ServerShared.Services.PluginService.Load">
<summary>
Loads an instance of the plugin service
</summary>
<returns>an instance of the plugin service</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.PluginService.Download(FileFlows.Shared.Models.PluginInfo)">
<summary>
Download a plugin
</summary>
<param name="plugin">the plugin to download</param>
<returns>the byte data of the plugin</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.PluginService.GetAll">
<summary>
Get all plugin infos
</summary>
<returns>all plugin infos</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.PluginService.GetSettingsJson(System.String)">
<summary>
Gets the settings json for a plugin
</summary>
<param name="pluginPackageName">the name of the plugin package</param>
<returns>the settings json</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.PluginService.Update(FileFlows.Shared.Models.PluginInfo)">
<summary>
Updates plugin info
</summary>
<param name="pluginInfo">the plugin info</param>
<returns>the updated plugininfo</returns>
<exception cref="T:System.NotImplementedException">This not yet implemented</exception>
</member>
<member name="T:FileFlows.ServerShared.Services.ISystemService">
<summary>
An interface of the System Service
</summary>
</member>
<member name="M:FileFlows.ServerShared.Services.ISystemService.GetVersion">
<summary>
Gets the version from the server
</summary>
<returns>the server version</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.ISystemService.GetNodeUpdater">
<summary>
Gets the node updater binary
</summary>
<returns>the node updater binary</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.ISystemService.GetNodeUpdateIfAvailable(System.String)">
<summary>
Gets an node update available
</summary>
<param name="version">the current version of the node</param>
<returns>if there is a node update available, returns the update</returns>
</member>
<member name="T:FileFlows.ServerShared.Services.SystemService">
<summary>
A System Service
</summary>
</member>
<member name="P:FileFlows.ServerShared.Services.SystemService.Loader">
<summary>
Gets or sets the loader for SystemService
</summary>
</member>
<member name="M:FileFlows.ServerShared.Services.SystemService.Load">
<summary>
Loads an instance of SystemService
</summary>
<returns>an instance of SystemService</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.SystemService.GetVersion">
<summary>
Gets the version from the server
</summary>
<returns>the server version</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.SystemService.GetNodeUpdater">
<summary>
Gets the node updater binary
</summary>
<returns>the node updater binary</returns>
</member>
<member name="M:FileFlows.ServerShared.Services.SystemService.GetNodeUpdateIfAvailable(System.String)">
<summary>
Gets an node update available
</summary>
<param name="version">the current version of the node</param>
<returns>if there is a node update available, returns the update</returns>
</member>
</members>
</doc>

View File

@@ -4,6 +4,31 @@
<name>FileFlows.Shared</name>
</assembly>
<members>
<member name="T:FileFlows.Shared.Helpers.ObjectCloner">
<summary>
Clones an object
</summary>
</member>
<member name="T:FileFlows.Shared.Helpers.ReferenceEqualityComparer">
<summary>
Checks if objects are teh same reference
</summary>
</member>
<member name="M:FileFlows.Shared.Helpers.ReferenceEqualityComparer.Equals(System.Object,System.Object)">
<summary>
Checks if two objects are equal
</summary>
<param name="x">first object</param>
<param name="y">second object</param>
<returns>If the objects are ruqla</returns>
</member>
<member name="M:FileFlows.Shared.Helpers.ReferenceEqualityComparer.GetHashCode(System.Object)">
<summary>
Gets a hashcode of an object
</summary>
<param name="obj">the object</param>
<returns>the objec hashcode</returns>
</member>
<member name="P:FileFlows.Shared.Models.ElementField.Placeholder">
<summary>
Gets or sets optional place holder text, this can be a translation key

View File

@@ -83,4 +83,5 @@ Emby
revenz
Fenrus
Plex-Token
analyze
analyze
px