From 2571ffd2ea7656c4f95ab3727ce602ff1e8115e9 Mon Sep 17 00:00:00 2001 From: reven Date: Thu, 27 Apr 2023 09:14:45 +1200 Subject: [PATCH] FF-460 - added imagemagick to image nodes --- ImageNodes/ImageNodes.csproj | Bin 3406 -> 3562 bytes ImageNodes/Images/AutoCropImage.cs | 23 +++++++- ImageNodes/Images/ImageBaseNode.cs | 91 +++++++++++++++++------------ ImageNodes/Images/ImageFile.cs | 4 +- ImageNodes/Images/ImageFlip.cs | 3 +- ImageNodes/Images/ImageFormat.cs | 23 ++++++-- ImageNodes/Images/ImageNode.cs | 58 +++++++++++++++++- ImageNodes/Images/ImageResizer.cs | 3 +- ImageNodes/Images/ImageRotate.cs | 3 +- ImageNodes/Tests/ImageTests.cs | 79 ++++++++++++++++++++++++- 10 files changed, 236 insertions(+), 51 deletions(-) diff --git a/ImageNodes/ImageNodes.csproj b/ImageNodes/ImageNodes.csproj index 75cad55a007b61bb97193bae3d5892dee68fe058..15a650b90abeca7e3a4061e603f4c634758d33e8 100644 GIT binary patch delta 73 zcmX>n^-6lfVIECihD3&RhD?TJhHM5s20sQ@h7bl_hCl{G1~VY-$dJcS$>7Wozz{n5 Z0+%?qA%ihcl>rbNPLALToP3z48vtgl5BvZC delta 11 ScmaDQeNJk_VV=qHyxjmG;RN>p diff --git a/ImageNodes/Images/AutoCropImage.cs b/ImageNodes/Images/AutoCropImage.cs index 26476a5e..495a175c 100644 --- a/ImageNodes/Images/AutoCropImage.cs +++ b/ImageNodes/Images/AutoCropImage.cs @@ -2,6 +2,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.ComponentModel; +using ImageMagick; namespace FileFlows.ImageNodes.Images; @@ -18,8 +19,28 @@ public class AutoCropImage : ImageNode [DefaultValue(50)] public int Threshold { get; set; } - public override int Execute(NodeParameters args) + => ExecuteImageMagick(args); + + private int ExecuteImageMagick(NodeParameters args) + { + using MagickImage image = new MagickImage(args.WorkingFile); + (int originalWidth, int originalHeight) = (image.Width, image.Height); + + // image magick threshold is reversed, 100 means dont trim much, 1 means trim a lot + image.Trim(new Percentage(100 - Threshold)); + + if (image.Width == originalWidth && image.Height == originalHeight) + return 2; + + var formatOpts = GetFormat(args); + SaveImage(args, image, formatOpts.file, updateWorkingFile:true); + args.Logger?.ILog($"Image cropped from '{originalWidth}x{originalHeight}' to '{image.Width}x{image.Height}'"); + + return 1; + } + + private int ExecuteImageSharp(NodeParameters args) { using var image = Image.Load(args.WorkingFile, out IImageFormat format); int originalWidth = image.Width; diff --git a/ImageNodes/Images/ImageBaseNode.cs b/ImageNodes/Images/ImageBaseNode.cs index c1921fa3..55b0a4fd 100644 --- a/ImageNodes/Images/ImageBaseNode.cs +++ b/ImageNodes/Images/ImageBaseNode.cs @@ -1,4 +1,5 @@ -using SixLabors.ImageSharp.Formats; +using ImageMagick; +using SixLabors.ImageSharp.Formats; namespace FileFlows.ImageNodes.Images; @@ -6,61 +7,56 @@ public abstract class ImageBaseNode:Node { private const string IMAGE_INFO = "ImageInfo"; - protected IImageFormat CurrentFormat { get; private set; } + protected string CurrentFormat { get; private set; } protected int CurrentWidth{ get; private set; } protected int CurrentHeight { get; private set; } public override bool PreExecute(NodeParameters args) { - using var image = Image.Load(args.WorkingFile, out IImageFormat format); - CurrentHeight = image.Height; - CurrentWidth = image.Width; - CurrentFormat = format; + if (args.WorkingFile.ToLowerInvariant().EndsWith(".heic")) + { + using var image = new MagickImage(args.WorkingFile); + CurrentHeight = image.Height; + CurrentWidth = image.Width; + CurrentFormat = "HEIC"; + } + else + { + using var image = Image.Load(args.WorkingFile, out IImageFormat format); + CurrentHeight = image.Height; + CurrentWidth = image.Width; + CurrentFormat = format.Name; + } var metadata = new Dictionary(); - metadata.Add("Format", CurrentFormat.Name); + metadata.Add("Format", CurrentFormat); metadata.Add("Width", CurrentWidth); metadata.Add("Height", CurrentHeight); args.SetMetadata(metadata); + return true; } protected void UpdateImageInfo(NodeParameters args, Dictionary variables = null) { - using var image = Image.Load(args.WorkingFile, out IImageFormat format); - var imageInfo = new ImageInfo + string extension = new FileInfo(args.WorkingFile).Extension[1..].ToLowerInvariant(); + if (extension == "heic") { - Width = image.Width, - Height = image.Height, - Format = format.Name - }; - - var metadata = new Dictionary(); - metadata.Add("Format", imageInfo.Format); - metadata.Add("Width", imageInfo.Width); - metadata.Add("Height", imageInfo.Height); - args.SetMetadata(metadata); - - variables ??= new Dictionary(); - if (args.Parameters.ContainsKey(IMAGE_INFO)) - args.Parameters[IMAGE_INFO] = imageInfo; + using var image = new MagickImage(args.WorkingFile); + UpdateImageInfo(args, image.Width, image.Height, "HEIC", variables); + } 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); + { + using var image = Image.Load(args.WorkingFile, out IImageFormat format); + UpdateImageInfo(args, image.Width, image.Height, format.Name, variables); + } } - protected void UpdateImageInfo(NodeParameters args, Image image, IImageFormat format, Dictionary variables = null) + protected void UpdateImageInfo(NodeParameters args, int width, int height, string format, Dictionary variables = null) { var imageInfo = new ImageInfo { - Width = image.Width, - Height = image.Height, - Format = format.Name + Width = width, + Height = height, + Format = format }; variables ??= new Dictionary(); @@ -84,7 +80,6 @@ public abstract class ImageBaseNode:Node args.UpdateVariables(variables); } - internal ImageInfo? GetImageInfo(NodeParameters args) { if (args.Parameters.ContainsKey(IMAGE_INFO) == false) @@ -100,4 +95,26 @@ public abstract class ImageBaseNode:Node } return result; } + + /// + /// Converts an image to a format we can use, if needed + /// + /// the node parameters + /// the filename fo the image to use + protected string ConvertImageIfNeeded(NodeParameters args) + { + string extension = new FileInfo(args.WorkingFile).Extension[1..].ToLowerInvariant(); + if (extension == "heic") + { + // special case have to use imagemagick + + using var image = new MagickImage(args.WorkingFile); + image.Format = MagickFormat.Png; + var newFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + ".png"); + image.Write(newFile); + return newFile; + } + + return args.WorkingFile; + } } diff --git a/ImageNodes/Images/ImageFile.cs b/ImageNodes/Images/ImageFile.cs index 7b68ecc1..cb79d0dc 100644 --- a/ImageNodes/Images/ImageFile.cs +++ b/ImageNodes/Images/ImageFile.cs @@ -29,8 +29,8 @@ public class ImageFile : ImageBaseNode try { UpdateImageInfo(args, this.Variables); - if(string.IsNullOrEmpty(base.CurrentFormat?.Name) == false) - args.RecordStatistic("IMAGE_FORMAT", base.CurrentFormat.Name); + if(string.IsNullOrEmpty(base.CurrentFormat) == false) + args.RecordStatistic("IMAGE_FORMAT", base.CurrentFormat); return 1; } diff --git a/ImageNodes/Images/ImageFlip.cs b/ImageNodes/Images/ImageFlip.cs index 9caba9eb..81f7b165 100644 --- a/ImageNodes/Images/ImageFlip.cs +++ b/ImageNodes/Images/ImageFlip.cs @@ -17,7 +17,8 @@ public class ImageFlip: ImageNode public override int Execute(NodeParameters args) { - using var image = Image.Load(args.WorkingFile, out IImageFormat format); + var input = ConvertImageIfNeeded(args); + using var image = Image.Load(input, out IImageFormat format); image.Mutate(c => c.Flip(Vertical ? FlipMode.Vertical : FlipMode.Horizontal)); var formatOpts = GetFormat(args); SaveImage(args, image, formatOpts.file, formatOpts.format ?? format); diff --git a/ImageNodes/Images/ImageFormat.cs b/ImageNodes/Images/ImageFormat.cs index d7de940e..d845ba2b 100644 --- a/ImageNodes/Images/ImageFormat.cs +++ b/ImageNodes/Images/ImageFormat.cs @@ -1,4 +1,5 @@ -using SixLabors.ImageSharp.Formats; +using ImageMagick; +using SixLabors.ImageSharp.Formats; namespace FileFlows.ImageNodes.Images; @@ -15,14 +16,26 @@ public class ImageFormat: ImageNode { var formatOpts = GetFormat(args); - if(formatOpts.format?.Name == CurrentFormat.Name) + if(formatOpts.format?.Name == CurrentFormat) { args.Logger?.ILog("File already in format: " + formatOpts.format.Name); return 2; } - using var image = Image.Load(args.WorkingFile, out IImageFormat format); - SaveImage(args, image, formatOpts.file, formatOpts.format ?? format); - return 1; + string extension = new FileInfo(args.WorkingFile).Extension[1..].ToLowerInvariant(); + if (extension == "heic") + { + // special case have to use imagemagick + + using var image = new MagickImage(args.WorkingFile); + SaveImage(args, image, formatOpts.file); + return 1; + } + else + { + using var image = Image.Load(args.WorkingFile, out IImageFormat format); + SaveImage(args, image, formatOpts.file, formatOpts.format ?? format); + return 1; + } } } diff --git a/ImageNodes/Images/ImageNode.cs b/ImageNodes/Images/ImageNode.cs index 01c4828a..aa95e250 100644 --- a/ImageNodes/Images/ImageNode.cs +++ b/ImageNodes/Images/ImageNode.cs @@ -1,3 +1,6 @@ +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using ImageMagick; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; @@ -78,8 +81,14 @@ public abstract class ImageNode : ImageBaseNode newFile = newFile + ".webp"; format = WebpFormat.Instance; break; + case "HEIC": + // cant save to this format, save to PNG + newFile = newFile + ".png"; + format = PngFormat.Instance; + break; default: newFile = newFile + "." + args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".") + 1); + newFile = Regex.Replace(newFile, @"\.heic$", ".png", RegexOptions.IgnoreCase); break; } @@ -93,7 +102,54 @@ public abstract class ImageNode : ImageBaseNode if (updateWorkingFile) { args.SetWorkingFile(file); - UpdateImageInfo(args, img, format, Variables); + UpdateImageInfo(args, img.Height, img.Height, format.Name, Variables); + } + } + + protected void SaveImage(NodeParameters args, ImageMagick.MagickImage img, string file, bool updateWorkingFile = true) + { + using Stream outStream = new FileStream(file, FileMode.Create); + string origExtension = new FileInfo(args.WorkingFile).Extension[1..].ToLowerInvariant(); + string newExtension = new FileInfo(file).Extension[1..].ToLowerInvariant(); + if (origExtension != newExtension) + { + switch (newExtension) + { + case "jpg": + case "jpeg": + img.Format = MagickFormat.Jpeg; + break; + case "png": + img.Format = MagickFormat.Png; + break; + case "gif": + img.Format = MagickFormat.Gif; + break; + case "bmp": + img.Format = MagickFormat.Bmp; + break; + case "tga": + img.Format = MagickFormat.Tga; + break; + case "webp": + img.Format = MagickFormat.WebP; + break; + case "webm": + img.Format = MagickFormat.WebM; + break; + default: + if (Enum.TryParse(newExtension, true, out MagickFormat format)) + img.Format = format; + break; + } + } + + img.Write(outStream); + if (updateWorkingFile) + { + args.SetWorkingFile(file); + using var image = Image.Load(args.WorkingFile, out IImageFormat format); + UpdateImageInfo(args, img.Width, img.Height, format.Name, Variables); } } } \ No newline at end of file diff --git a/ImageNodes/Images/ImageResizer.cs b/ImageNodes/Images/ImageResizer.cs index a59928e7..923c780b 100644 --- a/ImageNodes/Images/ImageResizer.cs +++ b/ImageNodes/Images/ImageResizer.cs @@ -55,7 +55,8 @@ public class ImageResizer: ImageNode public override int Execute(NodeParameters args) { - using var image = Image.Load(args.WorkingFile, out IImageFormat format); + string inputFile = ConvertImageIfNeeded(args); + using var image = Image.Load(inputFile, out IImageFormat format); SixLabors.ImageSharp.Processing.ResizeMode rzMode; switch (Mode) { diff --git a/ImageNodes/Images/ImageRotate.cs b/ImageNodes/Images/ImageRotate.cs index 36fd58b0..94805ff9 100644 --- a/ImageNodes/Images/ImageRotate.cs +++ b/ImageNodes/Images/ImageRotate.cs @@ -35,7 +35,8 @@ public class ImageRotate: ImageNode public override int Execute(NodeParameters args) { - using var image = Image.Load(args.WorkingFile, out IImageFormat format); + string inputFile = ConvertImageIfNeeded(args); + using var image = Image.Load(inputFile, out IImageFormat format); image.Mutate(c => c.Rotate(Angle)); var formatOpts = GetFormat(args); SaveImage(args, image, formatOpts.file, formatOpts.format ?? format); diff --git a/ImageNodes/Tests/ImageTests.cs b/ImageNodes/Tests/ImageTests.cs index 28eaeb67..d10f42f8 100644 --- a/ImageNodes/Tests/ImageTests.cs +++ b/ImageNodes/Tests/ImageTests.cs @@ -11,6 +11,7 @@ public class ImageNodesTests { string TestImage1; string TestImage2; + string TestImageHeic; string TempDir; string TestCropImage1, TestCropImage2, TestCropImage3, TestCropImage4, TestCropImageNoCrop; @@ -30,6 +31,10 @@ public class ImageNodesTests } else { + TestCropImage1 = "/home/john/Pictures/cropme2.jpg"; + TestCropImage2 = "/home/john/Pictures/cropme.jpg"; + TestCropImage3 = "/home/john/Pictures/crop.heic"; + TestImageHeic = "/home/john/Pictures/crop.heic"; TestImage1 = "/home/john/Pictures/fileflows.png"; TestImage2 = "/home/john/Pictures/36410427.png"; TempDir = "/home/john/src/temp/"; @@ -49,6 +54,35 @@ public class ImageNodesTests Assert.AreEqual(1, node.Execute(args)); } + [TestMethod] + public void ImageNodes_Basic_ImageFormat_Heic() + { + var args = new NodeParameters(TestImageHeic, new TestLogger(), false, string.Empty) + { + TempPath = TempDir + }; + + var node = new ImageFormat(); + node.Format = "HEIC"; + Assert.AreEqual(1, node.Execute(args)); + } + + [TestMethod] + public void ImageNodes_Basic_IsLandscape_Heic() + { + var args = new NodeParameters(TestImageHeic, new TestLogger(), false, string.Empty) + { + TempPath = TempDir + }; + + var imageNode = new ImageFile(); + imageNode.Execute(args); + + var node = new ImageIsLandscape(); + node.PreExecute(args); + Assert.AreEqual(1, node.Execute(args)); + } + [TestMethod] public void ImageNodes_Basic_Resize() { @@ -64,6 +98,21 @@ public class ImageNodesTests Assert.AreEqual(1, node.Execute(args)); } + [TestMethod] + public void ImageNodes_Basic_Resize_Heic() + { + var args = new NodeParameters(TestImageHeic, new TestLogger(), false, string.Empty) + { + TempPath = TempDir + }; + + var node = new ImageResizer(); + node.Width = 1000; + node.Height = 500; + node.Mode = ResizeMode.Fill; + Assert.AreEqual(1, node.Execute(args)); + } + [TestMethod] public void ImageNodes_Basic_Resize_Percent() { @@ -102,6 +151,19 @@ public class ImageNodesTests Assert.AreEqual(1, node.Execute(args)); } + + [TestMethod] + public void ImageNodes_Basic_Flip_Heic() + { + var args = new NodeParameters(TestImageHeic, new TestLogger(), false, string.Empty) + { + TempPath = TempDir + }; + + var node = new ImageFlip(); + node.Vertical = false; + Assert.AreEqual(1, node.Execute(args)); + } [TestMethod] public void ImageNodes_Basic_Rotate() @@ -116,10 +178,23 @@ public class ImageNodesTests Assert.AreEqual(1, node.Execute(args)); } + [TestMethod] + public void ImageNodes_Basic_Rotate_Heic() + { + var args = new NodeParameters(TestImageHeic, new TestLogger(), false, string.Empty) + { + TempPath = TempDir + }; + + var node = new ImageRotate(); + node.Angle = 270; + Assert.AreEqual(1, node.Execute(args)); + } [TestMethod] public void ImageNodes_Basic_AutoCrop_01() { + Assert.IsTrue(File.Exists(TestCropImage1)); var logger = new TestLogger(); var args = new NodeParameters(TestCropImage1, logger, false, string.Empty) { @@ -127,7 +202,7 @@ public class ImageNodesTests }; var node = new AutoCropImage(); - node.Threshold = 50; + node.Threshold = 30; node.PreExecute(args); int result = node.Execute(args); @@ -162,7 +237,7 @@ public class ImageNodesTests }; var node = new AutoCropImage(); - node.Threshold = 50; + node.Threshold = 30; node.PreExecute(args); int result = node.Execute(args);