Kavita/API/Services/ImageService.cs
Joseph Milazzo e0a2fc615f
UX Changes, Tasks, WebP, and More! (#1280)
* When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt.

* Some cleanup on the user preferences to remove some calls we don't need anymore.

* Removed old bulk cleanup bookmark code as it's no longer needed.

* Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented.

* Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features.

* Implemented the ability to bulk convert bookmarks (as many times as the user wants).

Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release.

* Tweaked the wording around the convert switch.

* Moved System actions to the task tab

* Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route.

* Fixed the unit tests
2022-05-23 16:19:52 -07:00

180 lines
6.3 KiB
C#

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
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);
/// <summary>
/// Creates a Thumbnail version of a base64 image
/// </summary>
/// <param name="encodedImage">base64 encoded image</param>
/// <param name="fileName"></param>
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
string CreateThumbnailFromBase64(string encodedImage, string fileName);
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory);
/// <summary>
/// Converts the passed image to webP and outputs it in the same directory
/// </summary>
/// <param name="filePath">Full path to the image to convert</param>
/// <returns>File of written webp image</returns>
Task<string> ConvertToWebP(string filePath, string outputPath);
}
public class ImageService : IImageService
{
private readonly ILogger<ImageService> _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+";
/// <summary>
/// Width of the Thumbnail generation
/// </summary>
private const int ThumbnailWidth = 320;
public ImageService(ILogger<ImageService> logger, IDirectoryService directoryService)
{
_logger = logger;
_directoryService = directoryService;
}
public void ExtractImages(string fileFilePath, string targetDirectory, int fileCount)
{
_directoryService.ExistOrCreate(targetDirectory);
if (fileCount == 1)
{
_directoryService.CopyFileToDirectory(fileFilePath, targetDirectory);
}
else
{
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(fileFilePath), targetDirectory,
Parser.Parser.ImageFileExtensions);
}
}
public string GetCoverImage(string path, string fileName, string outputDirectory)
{
if (string.IsNullOrEmpty(path)) return string.Empty;
try
{
using var thumbnail = Image.Thumbnail(path, ThumbnailWidth);
var filename = fileName + ".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;
}
/// <summary>
/// Creates a thumbnail out of a memory stream and saves to <see cref="DirectoryService.CoverImageDirectory"/> with the passed
/// fileName and .png extension.
/// </summary>
/// <param name="stream">Stream to write to disk. Ensure this is rewinded.</param>
/// <param name="fileName">filename to save as without extension</param>
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory)
{
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
var filename = fileName + ".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 async Task<string> ConvertToWebP(string filePath, string outputPath)
{
var file = _directoryService.FileSystem.FileInfo.FromFileName(filePath);
var fileName = file.Name.Replace(file.Extension, string.Empty);
var outputFile = Path.Join(outputPath, fileName + ".webp");
using var sourceImage = await SixLabors.ImageSharp.Image.LoadAsync(filePath);
await sourceImage.SaveAsWebpAsync(outputFile);
return outputFile;
}
/// <inheritdoc />
public string CreateThumbnailFromBase64(string encodedImage, string fileName)
{
try
{
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), ThumbnailWidth);
var filename = fileName + ".png";
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName + ".png"));
return filename;
}
catch (Exception e)
{
_logger.LogError(e, "Error creating thumbnail from url");
}
return string.Empty;
}
/// <summary>
/// Returns the name format for a chapter cover image
/// </summary>
/// <param name="chapterId"></param>
/// <param name="volumeId"></param>
/// <returns></returns>
public static string GetChapterFormat(int chapterId, int volumeId)
{
return $"v{volumeId}_c{chapterId}";
}
/// <summary>
/// Returns the name format for a series cover image
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public static string GetSeriesFormat(int seriesId)
{
return $"series{seriesId}";
}
/// <summary>
/// Returns the name format for a collection tag cover image
/// </summary>
/// <param name="tagId"></param>
/// <returns></returns>
public static string GetCollectionTagFormat(int tagId)
{
return $"tag{tagId}";
}
/// <summary>
/// Returns the name format for a reading list cover image
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
public static string GetReadingListFormat(int readingListId)
{
return $"readinglist{readingListId}";
}
}