Files
FileFlowsPlugins/ComicNodes/Comics/ComicConverter.cs
2024-05-08 17:29:54 +12:00

377 lines
13 KiB
C#

using System.ComponentModel;
using System.Diagnostics;
using FileFlows.Plugin.Helpers;
namespace FileFlows.ComicNodes.Comics;
/// <summary>
/// Convert comic books
/// </summary>
public class ComicConverter: Node
{
/// <inheritdoc />
public override int Inputs => 1;
/// <inheritdoc />
public override int Outputs => 2;
/// <inheritdoc />
public override FlowElementType Type => FlowElementType.Process;
/// <inheritdoc />
public override string Icon => "fas fa-book";
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/comic-nodes/comic-converter";
CancellationTokenSource cancellation = new CancellationTokenSource();
/// <summary>
/// Gets or sets the comic book format
/// </summary>
[DefaultValue("CBZ")]
[Select(nameof(FormatOptions), 1)]
public string Format { get; set; } = string.Empty;
private static List<ListOption>? _FormatOptions;
/// <summary>
/// Gets the format options
/// </summary>
public static List<ListOption> FormatOptions
{
get
{
if (_FormatOptions == null)
{
_FormatOptions = new List<ListOption>
{
new() { Label = "CBZ", Value = "CBZ" },
//new ListOption { Label = "CB7", Value = "cb7"},
new() { Label = "PDF", Value = "PDF" }
};
}
return _FormatOptions;
}
}
/// <summary>
/// Gets or sets if the archive should only have images in the top directgory
/// </summary>
[Boolean(2)]
[ConditionEquals(nameof(Format), "PDF", inverse:true)]
public bool EnsureTopDirectory { get; set; }
/// <summary>
/// Gets or sets if non page images should be deleted
/// </summary>
[Boolean(3)]
public bool DeleteNonPageImages { get; set; }
/// <summary>
/// Gets or sets the codec the images will be saved in
/// </summary>
[DefaultValue("")]
[Select(nameof(CodecOptions), 4)]
public string Codec { get; set; } = string.Empty;
private static List<ListOption>? _CodecOptions;
/// <summary>
/// Gets the format options
/// </summary>
public static List<ListOption> CodecOptions
{
get
{
if (_CodecOptions == null)
{
_CodecOptions = new List<ListOption>
{
new() { Label = "Same as source", Value = "" },
new() { Label = "JPEG", Value = "jpeg" },
new() { Label = "WEBP", Value = "webp" }
};
}
return _CodecOptions;
}
}
/// <summary>
/// Gets or sets the quality
/// </summary>
[Range(0, 100)]
[Slider(5)]
[DefaultValue(75)]
[ConditionEquals(nameof(Codec), "", true)]
public int Quality { get; set; }
/// <summary>
/// Gets or sets the maximum width of images
/// </summary>
[NumberInt(6)]
[ConditionEquals(nameof(Codec), "", true)]
public int MaxWidth { get; set; }
/// <summary>
/// Gets or sets the maximum height of images
/// </summary>
[NumberInt(7)]
[ConditionEquals(nameof(Codec), "", true)]
public int MaxHeight { get; set; }
/// <inheritdoc />
public override int Execute(NodeParameters args)
{
var localFileResult = args.FileService.GetLocalPath(args.WorkingFile);
if (localFileResult.Failed(out var error))
{
args.FailureReason = "Failed getting local file: " + error;
args.Logger?.ELog(args.FailureReason);
return -1;
}
var localFile = localFileResult.Value;
string currentFormat = new FileInfo(args.WorkingFile).Extension;
if (string.IsNullOrEmpty(currentFormat))
{
args.Logger?.ELog("Could not detect format for: " + args.WorkingFile);
return -1;
}
Format = Format?.ToUpper() ?? string.Empty;
if (currentFormat[0] == '.')
currentFormat = currentFormat[1..]; // remove the dot
currentFormat = currentFormat.ToUpper();
args.Logger?.ILog("Current Format: " + currentFormat);
args.Logger?.ILog("Desired Format: " + Format);
var metadata = new Dictionary<string, object>();
metadata.Add("Format", currentFormat);
var pageCountResult = GetPageCount(args, currentFormat, localFile);
if (pageCountResult.Success(out int pageCount))
{
args.Logger?.ILog("Page Count: " + pageCount);
metadata.Add("Pages", pageCount);
args.RecordStatisticAverage("COMIC_PAGES", pageCount);
}
args.RecordStatisticRunningTotals("COMIC_FORMAT", currentFormat);
args.SetMetadata(metadata);
args.Logger?.ILog("Setting metadata: " + currentFormat);
if (currentFormat == Format && string.IsNullOrWhiteSpace(Codec) &&
(currentFormat.ToLowerInvariant() == "pdf" || EnsureTopDirectory == false))
{
args.Logger?.ILog($"Already in the target format of '{Format}'");
return 2;
}
string destinationPath = Path.Combine(args.TempPath, Guid.NewGuid().ToString());
var rgxImages = new Regex(@"\.(jpeg|jpg|jp2|jpe|png|bmp|tiff|webp|gif)$", RegexOptions.IgnoreCase);
Directory.CreateDirectory(destinationPath);
if (Helpers.ComicExtractor
.Extract(args, localFile, destinationPath, halfProgress: true, cancellation: cancellation.Token)
.Failed(out error))
{
args.FailureReason = "Failed to extract comic: " + error;
args.Logger?.ELog(args.FailureReason);
return -1;
}
if (DeleteNonPageImages)
{
foreach (var file in Directory.GetFiles(destinationPath, "*.*", SearchOption.AllDirectories))
{
if(rgxImages.IsMatch(file) == false)
continue;
string nameNoExtension = FileHelper.GetShortFileName(file);
nameNoExtension = nameNoExtension[..nameNoExtension.LastIndexOf(".", StringComparison.Ordinal)];
if (Regex.IsMatch(nameNoExtension, @"[\d]{2,}$") == false)
{
args.Logger?.ILog("Deleting non page image: " + file);
File.Delete(file);
}
}
}
if (EnsureTopDirectory)
{
args.Logger?.ILog("Ensuring top directory");
MoveSubFilesToRootDirectory(destinationPath);
}
if (Codec.ToLowerInvariant() is "webp" or "jpeg")
{
args.Logger?.ILog("Converting images to: " + Codec);
var files = Directory.GetFiles(destinationPath, "*.*", SearchOption.AllDirectories);
ImageOptions imageOptions = new()
{
Quality = Quality,
MaxWidth = MaxWidth,
MaxHeight = MaxHeight
};
args.Logger?.ILog("Quality: " + Quality);
args.Logger?.ILog("MaxWidth: " + MaxWidth);
args.Logger?.ILog("MaxHeight: " + MaxHeight);
args.Logger?.ILog("Total Files: " + files.Length);
args.PartPercentageUpdate?.Invoke(0);
int count = 0;
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
if (cancellation.IsCancellationRequested)
break;
try
{
if (File.Exists(file) == false)
continue; // may have been replaced
if (file.ToLowerInvariant().EndsWith(".pdf"))
{
args.Logger?.ILog("Deleting: " + file);
File.Delete(file);
continue;
}
if (rgxImages.IsMatch(file) == false)
continue;
DateTime dt = DateTime.UtcNow;
string dest;
if (Codec.ToLowerInvariant() == "webp")
{
dest = Path.ChangeExtension(file, "webp");
args.ImageHelper.ConvertToWebp(file, dest, imageOptions);
}
else
{
dest = Path.ChangeExtension(file, "jpg");
args.ImageHelper.ConvertToJpeg(file, dest, imageOptions);
}
if (File.Exists(dest) == false)
{
args.FailureReason = "Failed to convert image: " + dest;
args.Logger?.ELog(args.FailureReason);
break;
}
args.Logger?.ILog($"Converted image [{DateTime.UtcNow.Subtract(dt)}]: {dest}");
if (File.Exists(file) && file != dest)
{
args.Logger?.ILog("Deleting file: " + file);
File.Delete(file);
}
}
finally
{
float total = files.Length;
args.PartPercentageUpdate?.Invoke((count++ / total) * 100);
}
}
}
if (cancellation.IsCancellationRequested)
return -1;
string newFile = CreateComic(args, destinationPath, this.Format);
args.SetWorkingFile(newFile);
return 1;
}
/// <summary>
/// Cancels the conversion
/// </summary>
/// <returns>the task to await</returns>
public override Task Cancel()
{
cancellation.Cancel();
return base.Cancel();
}
/// <summary>
/// Gets the total number of pages
/// </summary>
/// <param name="args">the node parameters</param>
/// <param name="format">the format</param>
/// <param name="file">the file to get the page count for</param>
/// <returns>the number of pages</returns>
private Result<int> GetPageCount(NodeParameters args, string format, string file)
{
if (format == null)
return 0;
format = format.ToUpper().Trim();
switch (format)
{
case "PDF":
return Helpers.PdfHelper.GetPageCount(file);
default:
return args.ArchiveHelper.GetFileCount(file,@"\.(jpeg|jpg|jpe|jp2|png|bmp|tiff|webp|gif)$");
}
}
/// <summary>
/// Creates the comic
/// </summary>
/// <param name="args">the node parameters</param>
/// <param name="directory">the directory to create the comic out of</param>
/// <param name="format">the format to create the comic</param>
/// <returns>the path to the newly created comic</returns>
/// <exception cref="Exception">if the format is not supported</exception>
private string CreateComic(NodeParameters args, string directory, string format)
{
string file = Path.Combine(args.TempPath, Guid.NewGuid() + "." + format.ToLower());
args.Logger?.ILog("Creating comic: " + file);
if (format == "CBZ")
args.ArchiveHelper.Compress(directory, file);
//else if (format == "CB7")
// Helpers.SevenZipHelper.Compress(args, directory, file + ".7z");
else if (format == "PDF")
Helpers.PdfHelper.Create(args, directory, file);
else
throw new Exception("Unknown format:" + format);
Directory.Delete(directory, true);
args.Logger?.ILog("Created comic: " + file);
args.Logger?.ILog("Deleted temporary extraction directory: " + directory);
var metadata = new Dictionary<string, object>();
metadata.Add("Format", format);
if(GetPageCount(args, format, file).Success(out var count))
metadata.Add("Pages", count);
args.SetMetadata(metadata);
args.Logger?.ILog("Setting metadata: " + format);
return file;
}
/// <summary>
/// Moves all files from subdirectories of the specified directory to the root directory,
/// overwriting duplicates, and deletes empty subdirectories.
/// </summary>
/// <param name="path">The directory containing subdirectories with files to be moved.</param>
static void MoveSubFilesToRootDirectory(string path)
{
if (Directory.Exists(path) == false)
return;
foreach (var subDirectory in Directory.GetDirectories(path))
{
foreach (var filePath in Directory.GetFiles(subDirectory))
{
var fileName = Path.GetFileName(filePath);
var destinationFilePath = Path.Combine(path, fileName);
File.Copy(filePath, destinationFilePath, true);
}
}
foreach (string subDirectory in Directory.GetDirectories(path))
{
Directory.Delete(subDirectory, true);
}
}
}