From e6813b8e232973e33f2e3490827381a897ef91df Mon Sep 17 00:00:00 2001 From: John Andrews Date: Tue, 9 Aug 2022 11:24:07 +1200 Subject: [PATCH] FF-273 - added auto crop image node fixed issue with mp4 image based subtitles --- ComicNodes/Comics/ComicConverter.cs | 24 +++++-- ComicNodes/Comics/ComicExtractor.cs | 11 ++- ComicNodes/Helpers/ComicExtractor.cs | 53 +++++++------- ComicNodes/Helpers/PdfHelper.cs | 4 +- ImageNodes/ImageNodes.en.json | 13 ++++ ImageNodes/Images/AutoCropImage.cs | 42 +++++++++++ ImageNodes/Tests/ImageTests.cs | 71 +++++++++++++++++++ .../Models/FfmpegSubtitleStream.cs | 9 ++- VideoNodes/Helpers/SubtitleHelper.cs | 21 ++++++ VideoNodes/VideoInfoHelper.cs | 6 +- 10 files changed, 215 insertions(+), 39 deletions(-) create mode 100644 ImageNodes/Images/AutoCropImage.cs create mode 100644 VideoNodes/Helpers/SubtitleHelper.cs diff --git a/ComicNodes/Comics/ComicConverter.cs b/ComicNodes/Comics/ComicConverter.cs index dcfdf7f5..58e65885 100644 --- a/ComicNodes/Comics/ComicConverter.cs +++ b/ComicNodes/Comics/ComicConverter.cs @@ -10,6 +10,7 @@ public class ComicConverter: Node public override string Icon => "fas fa-book"; public override bool FailureNode => true; + CancellationTokenSource cancellation = new CancellationTokenSource(); [DefaultValue("cbz")] [Select(nameof(FormatOptions), 1)] @@ -24,9 +25,9 @@ public class ComicConverter: Node { _FormatOptions = new List { - new ListOption { Label = "CBZ", Value = "cbz"}, + new ListOption { Label = "CBZ", Value = "CBZ"}, //new ListOption { Label = "CB7", Value = "cb7"}, - new ListOption { Label = "PDF", Value = "pdf" } + new ListOption { Label = "PDF", Value = "PDF" } }; } return _FormatOptions; @@ -41,9 +42,11 @@ public class ComicConverter: Node args.Logger?.ELog("Could not detect format for: " + args.WorkingFile); return -1; } - if(currentFormat[0] == '.') + Format = Format?.ToUpper() ?? string.Empty; + + if (currentFormat[0] == '.') currentFormat = currentFormat[1..]; // remove the dot - currentFormat = currentFormat.ToLower(); + currentFormat = currentFormat.ToUpper(); var metadata = new Dictionary(); @@ -62,8 +65,9 @@ public class ComicConverter: Node } string destinationPath = Path.Combine(args.TempPath, Guid.NewGuid().ToString()); + Directory.CreateDirectory(destinationPath); - if (Helpers.ComicExtractor.Extract(args, args.WorkingFile, destinationPath, halfProgress: true) == false) + if (Helpers.ComicExtractor.Extract(args, args.WorkingFile, destinationPath, halfProgress: true, cancellation: cancellation.Token) == false) return -1; string newFile = CreateComic(args, destinationPath, this.Format); @@ -73,14 +77,20 @@ public class ComicConverter: Node return 1; } + public override Task Cancel() + { + cancellation.Cancel(); + return base.Cancel(); + } + private int GetPageCount(string format, string workingFile) { if (format == null) return 0; - format = format.ToLower().Trim(); + format = format.ToUpper().Trim(); switch (format) { - case "pdf": + case "PDF": return Helpers.PdfHelper.GetPageCount(workingFile); default: return Helpers.GenericExtractor.GetImageCount(workingFile); diff --git a/ComicNodes/Comics/ComicExtractor.cs b/ComicNodes/Comics/ComicExtractor.cs index 98d3887f..871422e6 100644 --- a/ComicNodes/Comics/ComicExtractor.cs +++ b/ComicNodes/Comics/ComicExtractor.cs @@ -9,6 +9,8 @@ public class ComicExtractor : Node public override FlowElementType Type => FlowElementType.Process; public override string Icon => "fas fa-file-pdf"; + CancellationTokenSource cancellation = new CancellationTokenSource(); + [Required] [Folder(1)] public string DestinationPath { get; set; } @@ -24,14 +26,19 @@ public class ComicExtractor : Node args.Result = NodeResult.Failure; return -1; } - Helpers.ComicExtractor.Extract(args, args.WorkingFile, dest, halfProgress: false); + Helpers.ComicExtractor.Extract(args, args.WorkingFile, dest, halfProgress: false, cancellation: cancellation.Token); var metadata = new Dictionary(); - metadata.Add("Format", args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".") + 1)); + metadata.Add("Format", args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".") + 1).ToUpper()); var rgxImages = new Regex(@"\.(jpeg|jpg|jpe|png|bmp|tiff|webp|gif)$"); metadata.Add("Pages", Directory.GetFiles(dest, "*.*", SearchOption.AllDirectories).Where(x => rgxImages.IsMatch(x)).Count()); args.SetMetadata(metadata); return 1; } + public override Task Cancel() + { + cancellation.Cancel(); + return base.Cancel(); + } } diff --git a/ComicNodes/Helpers/ComicExtractor.cs b/ComicNodes/Helpers/ComicExtractor.cs index 39f98a93..627ca736 100644 --- a/ComicNodes/Helpers/ComicExtractor.cs +++ b/ComicNodes/Helpers/ComicExtractor.cs @@ -4,36 +4,35 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace FileFlows.ComicNodes.Helpers +namespace FileFlows.ComicNodes.Helpers; + +internal class ComicExtractor { - internal class ComicExtractor + internal static bool Extract(NodeParameters args, string file, string destinationPath, bool halfProgress, CancellationToken cancellation) { - internal static bool Extract(NodeParameters args, string file, string destinationPath, bool halfProgress) + + string currentFormat = new FileInfo(args.WorkingFile).Extension; + if (string.IsNullOrEmpty(currentFormat)) { - - string currentFormat = new FileInfo(args.WorkingFile).Extension; - if (string.IsNullOrEmpty(currentFormat)) - { - args.Logger?.ELog("Could not detect format for: " + args.WorkingFile); - return false; - } - if (currentFormat[0] == '.') - currentFormat = currentFormat[1..]; // remove the dot - currentFormat = currentFormat.ToLower(); - - Directory.CreateDirectory(destinationPath); - args.Logger?.ILog("Extracting comic pages to: " + destinationPath); - - if (currentFormat == "pdf") - PdfHelper.Extract(args, args.WorkingFile, destinationPath, "page", halfProgress: halfProgress); - else if (currentFormat == "cbz") - ZipHelper.Extract(args, args.WorkingFile, destinationPath, halfProgress: halfProgress); - else if (currentFormat == "cb7" || currentFormat == "cbr" || currentFormat == "gz" || currentFormat == "bz2") - GenericExtractor.Extract(args, args.WorkingFile, destinationPath, halfProgress: halfProgress); - else - throw new Exception("Unknown format:" + currentFormat); - - return true; + args.Logger?.ELog("Could not detect format for: " + args.WorkingFile); + return false; } + if (currentFormat[0] == '.') + currentFormat = currentFormat[1..]; // remove the dot + currentFormat = currentFormat.ToUpper(); + + Directory.CreateDirectory(destinationPath); + args.Logger?.ILog("Extracting comic pages to: " + destinationPath); + + if (currentFormat == "PDF") + PdfHelper.Extract(args, args.WorkingFile, destinationPath, "page", halfProgress: halfProgress, cancellation: cancellation); + else if (currentFormat == "CBZ") + ZipHelper.Extract(args, args.WorkingFile, destinationPath, halfProgress: halfProgress); + else if (currentFormat == "CB7" || currentFormat == "CBR" || currentFormat == "GZ" || currentFormat == "BZ2") + GenericExtractor.Extract(args, args.WorkingFile, destinationPath, halfProgress: halfProgress); + else + throw new Exception("Unknown format:" + currentFormat); + + return true; } } diff --git a/ComicNodes/Helpers/PdfHelper.cs b/ComicNodes/Helpers/PdfHelper.cs index 20a3ae56..20c3fa50 100644 --- a/ComicNodes/Helpers/PdfHelper.cs +++ b/ComicNodes/Helpers/PdfHelper.cs @@ -9,7 +9,7 @@ namespace FileFlows.ComicNodes.Helpers; internal class PdfHelper { - public static void Extract(NodeParameters args, string pdfFile, string destinationDirectory, string filePrefix, bool halfProgress = true) + public static void Extract(NodeParameters args, string pdfFile, string destinationDirectory, string filePrefix, bool halfProgress, CancellationToken cancellation) { using var library = DocLib.Instance; using var docReader = library.GetDocReader(pdfFile, new PageDimensions(1080, 1920)); @@ -37,6 +37,8 @@ internal class PdfHelper percent = (percent / 2); args?.PartPercentageUpdate(percent); } + if (cancellation.IsCancellationRequested) + return; } if (args?.PartPercentageUpdate != null) args?.PartPercentageUpdate(halfProgress ? 50 : 0); diff --git a/ImageNodes/ImageNodes.en.json b/ImageNodes/ImageNodes.en.json index 74f1fb15..75e4694b 100644 --- a/ImageNodes/ImageNodes.en.json +++ b/ImageNodes/ImageNodes.en.json @@ -1,6 +1,19 @@ { "Flow": { "Parts": { + "AutoCropImage": { + "Outputs": { + "1": "Image cropped, saved to new temporary file", + "2": "Image was not cropped" + }, + "Description": "Automatically crops an image", + "Fields": { + "Format": "Format", + "Format-Help": "The image format to convert to", + "Threshold": "Threshold", + "Threshold-Help": "Threshold for entropic density, default is 50. Must be between 0 and 100." + } + }, "ImageFlip": { "Outputs": { "1": "Image flipped, saved to new temporary file" diff --git a/ImageNodes/Images/AutoCropImage.cs b/ImageNodes/Images/AutoCropImage.cs new file mode 100644 index 00000000..75dd98ee --- /dev/null +++ b/ImageNodes/Images/AutoCropImage.cs @@ -0,0 +1,42 @@ +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Processing; +using System.ComponentModel; + +namespace FileFlows.ImageNodes.Images; + +public class AutoCropImage : ImageNode +{ + public override int Inputs => 1; + public override int Outputs => 2; + public override FlowElementType Type => FlowElementType.Process; + public override string HelpUrl => "https://docs.fileflows.com/plugins/image-nodes/auto-crop-image"; + public override string Icon => "fas fa-crop"; + + [Slider(1)] + [Range(1, 100)] + [DefaultValue(50)] + public int Threshold { get; set; } + + + public override int Execute(NodeParameters args) + { + using var image = Image.Load(args.WorkingFile, out IImageFormat format); + int originalWidth = image.Width; + int originalHeight= image.Height; + float threshold = Threshold / 100f; + if (threshold < 0) + threshold = 0.5f; + + args.Logger?.ILog("Attempting to auto crop using threshold: " + threshold); + image.Mutate(c => c.EntropyCrop(threshold)); + + if (image.Width == originalWidth && image.Height == originalHeight) + return 2; + + var formatOpts = GetFormat(args); + SaveImage(args, image, formatOpts.file, formatOpts.format ?? format); + args.Logger?.ILog($"Image cropped from '{originalWidth}x{originalHeight}' to '{image.Width}x{image.Height}'"); + + return 1; + } +} diff --git a/ImageNodes/Tests/ImageTests.cs b/ImageNodes/Tests/ImageTests.cs index ed540bbf..142dbf18 100644 --- a/ImageNodes/Tests/ImageTests.cs +++ b/ImageNodes/Tests/ImageTests.cs @@ -12,6 +12,7 @@ public class ImageNodesTests string TestImage1; string TestImage2; string TempDir; + string TestCropImage1, TestCropImage2, TestCropImage3, TestCropImageNoCrop; public ImageNodesTests() { @@ -21,6 +22,10 @@ public class ImageNodesTests TestImage1 = @"D:\videos\pictures\image1.jpg"; TestImage2 = @"D:\videos\pictures\image2.png"; TempDir = @"D:\videos\temp"; + TestCropImage1 = @"D:\images\testimages\crop01.jpg"; + TestCropImage2 = @"D:\images\testimages\crop02.jpg"; + TestCropImage3 = @"D:\images\testimages\crop03.jpg"; + TestCropImageNoCrop = @"D:\images\testimages\nocrop.jpg"; } else { @@ -109,6 +114,72 @@ public class ImageNodesTests node.Angle = 270; Assert.AreEqual(1, node.Execute(args)); } + + + [TestMethod] + public void ImageNodes_Basic_AutoCrop_01() + { + var logger = new TestLogger(); + var args = new NodeParameters(TestCropImage1, logger, false, string.Empty) + { + TempPath = TempDir + }; + + var node = new AutoCropImage(); + int result = node.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void ImageNodes_Basic_AutoCrop_02() + { + var logger = new TestLogger(); + var args = new NodeParameters(TestCropImage2, logger, false, string.Empty) + { + TempPath = TempDir + }; + + var node = new AutoCropImage(); + node.Threshold = 95; + int result = node.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void ImageNodes_Basic_AutoCrop_03() + { + var logger = new TestLogger(); + var args = new NodeParameters(TestCropImage3, logger, false, string.Empty) + { + TempPath = TempDir + }; + + var node = new AutoCropImage(); + int result = node.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void ImageNodes_Basic_AutoCrop_NoCrop() + { + var logger = new TestLogger(); + var args = new NodeParameters(TestCropImageNoCrop, logger, false, string.Empty) + { + TempPath = TempDir + }; + + var node = new AutoCropImage(); + int result = node.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(2, result); + } } #endif \ No newline at end of file diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs index 35510ab0..114678ba 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs @@ -25,7 +25,14 @@ break; case "mp4": { - results.Add("mov_text"); + if (Helpers.SubtitleHelper.IsImageSubtitle(Stream.Codec)) + { + results.Add("copy"); + } + else + { + results.Add("mov_text"); + } } break; default: diff --git a/VideoNodes/Helpers/SubtitleHelper.cs b/VideoNodes/Helpers/SubtitleHelper.cs new file mode 100644 index 00000000..32b219ce --- /dev/null +++ b/VideoNodes/Helpers/SubtitleHelper.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileFlows.VideoNodes.Helpers; + +/// +/// Helper for Subtitles +/// +internal class SubtitleHelper +{ + /// + /// Tests if a subtitle is an image based subtitle + /// + /// the subtitle codec + /// true if the subtitle is an image based subtitle + internal static bool IsImageSubtitle(string codec) + => Regex.IsMatch(codec.Replace("_", ""), "dvbsub|dvdsub|pgs|xsub", RegexOptions.IgnoreCase); +} diff --git a/VideoNodes/VideoInfoHelper.cs b/VideoNodes/VideoInfoHelper.cs index bf0f67b2..6dbdab33 100644 --- a/VideoNodes/VideoInfoHelper.cs +++ b/VideoNodes/VideoInfoHelper.cs @@ -277,7 +277,9 @@ namespace FileFlows.VideoNodes audio.Title = ""; // this isnt type index, this is overall index audio.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value) - 1; - audio.Codec = parts[0].Substring(parts[0].IndexOf("Audio: ") + "Audio: ".Length).Trim().Split(' ').First().ToLower() ?? ""; + audio.Codec = parts[0].Substring(parts[0].IndexOf("Audio: ") + "Audio: ".Length).Trim().Split(' ').First().ToLower() ?? string.Empty; + if (audio.Codec.EndsWith(",")) + audio.Codec = audio.Codec[..^1].Trim(); audio.Language = GetLanguage(line); if (info.IndexOf("0 channels") >= 0) @@ -336,6 +338,8 @@ namespace FileFlows.VideoNodes SubtitleStream sub = new SubtitleStream(); sub.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value); sub.Codec = line.Substring(line.IndexOf("Subtitle: ") + "Subtitle: ".Length).Trim().Split(' ').First().ToLower(); + if (sub.Codec.EndsWith(",")) + sub.Codec = sub.Codec[..^1].Trim(); sub.Language = GetLanguage(line); if (rgxTitle.IsMatch(info))