using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetVips; using Image = NetVips.Image; namespace API.Services; public interface IImageService { void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); string GetCoverImage(string path, string fileName, string outputDirectory, bool saveAsWebP = false); /// /// Creates a Thumbnail version of a base64 image /// /// base64 encoded image /// /// Convert and save as webp /// Width of thumbnail /// File name with extension of the file. This will always write to string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320); /// /// Writes out a thumbnail by stream input /// /// /// /// /// /// string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false); /// /// Writes out a thumbnail by file path input /// /// /// /// /// /// string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false); /// /// Converts the passed image to webP and outputs it in the same directory /// /// Full path to the image to convert /// Where to output the file /// File of written webp image Task ConvertToWebP(string filePath, string outputPath); Task IsImage(string filePath); } public class ImageService : IImageService { private readonly ILogger _logger; private readonly IDirectoryService _directoryService; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"series\d+"; public const string CollectionTagCoverImageRegex = @"tag\d+"; public const string ReadingListCoverImageRegex = @"readinglist\d+"; /// /// Width of the Thumbnail generation /// private const int ThumbnailWidth = 320; /// /// Width of a cover for Library /// public const int LibraryThumbnailWidth = 32; public ImageService(ILogger logger, IDirectoryService directoryService) { _logger = logger; _directoryService = directoryService; } public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) { if (string.IsNullOrEmpty(fileFilePath)) return; _directoryService.ExistOrCreate(targetDirectory); if (fileCount == 1) { _directoryService.CopyFileToDirectory(fileFilePath, targetDirectory); } else { _directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, Tasks.Scanner.Parser.Parser.ImageFileExtensions); } } public string GetCoverImage(string path, string fileName, string outputDirectory, bool saveAsWebP = false) { if (string.IsNullOrEmpty(path)) return string.Empty; try { using var thumbnail = Image.Thumbnail(path, ThumbnailWidth); var filename = fileName + (saveAsWebP ? ".webp" : ".png"); thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } catch (Exception ex) { _logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {ImageFile}. Defaulting to no cover image", path); } return string.Empty; } /// /// Creates a thumbnail out of a memory stream and saves to with the passed /// fileName and .png extension. /// /// Stream to write to disk. Ensure this is rewinded. /// filename to save as without extension /// Where to output the file, defaults to covers directory /// Export the file as webP otherwise will default to png /// File name with extension of the file. This will always write to public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false) { using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth); var filename = fileName + (saveAsWebP ? ".webp" : ".png"); _directoryService.ExistOrCreate(outputDirectory); try { _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false) { using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth); var filename = fileName + (saveAsWebP ? ".webp" : ".png"); _directoryService.ExistOrCreate(outputDirectory); try { _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } public Task ConvertToWebP(string filePath, string outputPath) { var file = _directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + ".webp"); using var sourceImage = Image.NewFromFile(filePath, false, Enums.Access.SequentialUnbuffered); sourceImage.WriteToFile(outputFile); return Task.FromResult(outputFile); } /// /// Performs I/O to determine if the file is a valid Image /// /// /// public async Task IsImage(string filePath) { try { var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath); if (info == null) return false; return true; } catch (Exception) { /* Swallow Exception */ } return false; } /// public string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = ThumbnailWidth) { try { using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); fileName += (saveAsWebP ? ".webp" : ".png"); thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName)); return fileName; } catch (Exception e) { _logger.LogError(e, "Error creating thumbnail from url"); } return string.Empty; } /// /// Returns the name format for a chapter cover image /// /// /// /// public static string GetChapterFormat(int chapterId, int volumeId) { return $"v{volumeId}_c{chapterId}"; } /// /// Returns the name format for a library cover image /// /// /// public static string GetLibraryFormat(int libraryId) { return $"l{libraryId}"; } /// /// Returns the name format for a series cover image /// /// /// public static string GetSeriesFormat(int seriesId) { return $"series{seriesId}"; } /// /// Returns the name format for a collection tag cover image /// /// /// public static string GetCollectionTagFormat(int tagId) { return $"tag{tagId}"; } /// /// Returns the name format for a reading list cover image /// /// /// public static string GetReadingListFormat(int readingListId) { return $"readinglist{readingListId}"; } /// /// Returns the name format for a thumbnail (temp thumbnail) /// /// /// public static string GetThumbnailFormat(int chapterId) { return $"thumbnail{chapterId}"; } public static string CreateMergedImage(List coverImages, string dest) { // TODO: Needs testing // Currently this doesn't work due to non-standard cover image sizes and dimensions var image = Image.Black(320*4, 160*4); for (var i = 0; i < coverImages.Count; i++) { var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential); var x = (i % 2) * (image.Width / 2); var y = (i / 2) * (image.Height / 2); image = image.Insert(tile, x, y); } image.WriteToFile(dest); return dest; } }