FF-273 - added auto crop image node

fixed issue with mp4 image based subtitles
This commit is contained in:
John Andrews
2022-08-09 11:24:07 +12:00
parent 1131d6be72
commit e6813b8e23
10 changed files with 215 additions and 39 deletions

View File

@@ -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<ListOption>
{
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<string, object>();
@@ -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);

View File

@@ -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<string, object>();
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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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"

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FileFlows.VideoNodes.Helpers;
/// <summary>
/// Helper for Subtitles
/// </summary>
internal class SubtitleHelper
{
/// <summary>
/// Tests if a subtitle is an image based subtitle
/// </summary>
/// <param name="codec">the subtitle codec</param>
/// <returns>true if the subtitle is an image based subtitle</returns>
internal static bool IsImageSubtitle(string codec)
=> Regex.IsMatch(codec.Replace("_", ""), "dvbsub|dvdsub|pgs|xsub", RegexOptions.IgnoreCase);
}

View File

@@ -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))