From e2a86c67ec964cc7ce9801edcececcf51093b063 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 15 Aug 2024 08:25:39 -0300 Subject: [PATCH 01/37] Add JXL Support. --- .../ApplicationServiceExtensions.cs | 7 ++- API/Services/ArchiveService.cs | 12 ++-- .../JpegXLImageConverterProvider.cs | 29 +++++++++ API/Services/ImageConverterService.cs | 62 +++++++++++++++++++ API/Services/ImageService.cs | 8 ++- API/Services/Tasks/Scanner/Parser/Parser.cs | 2 +- Kavita.Common/EnvironmentInfo/IOsInfo.cs | 6 +- 7 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 API/Services/ImageConversion/JpegXLImageConverterProvider.cs create mode 100644 API/Services/ImageConverterService.cs diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index f68b4461d1..011acffc51 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,8 +1,9 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using API.Constants; using API.Data; using API.Helpers; using API.Services; +using API.Services.ImageConversion; using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Metadata; @@ -33,6 +34,10 @@ public static void AddApplicationServices(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 120cbf3f7d..8a42844020 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -58,15 +58,17 @@ public class ArchiveService : IArchiveService private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly IMediaErrorService _mediaErrorService; + private readonly IImageConverterService _converterService; private const string ComicInfoFilename = "ComicInfo.xml"; public ArchiveService(ILogger logger, IDirectoryService directoryService, - IImageService imageService, IMediaErrorService mediaErrorService) + IImageService imageService, IMediaErrorService mediaErrorService, IImageConverterService converterService) { _logger = logger; _directoryService = directoryService; _imageService = imageService; _mediaErrorService = mediaErrorService; + _converterService = converterService; } /// @@ -231,7 +233,7 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, archive.Entries.Select(e => e.FullName)); var entry = archive.Entries.Single(e => e.FullName == entryName); - using var stream = entry.Open(); + using var stream = _converterService.ConvertStream(entryName, entry.Open()); return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: @@ -242,7 +244,7 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, entryNames); var entry = archive.Entries.Single(e => e.Key == entryName); - using var stream = entry.OpenEntryStream(); + using var stream = _converterService.ConvertStream(entryName, entry.OpenEntryStream()); return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: @@ -560,6 +562,7 @@ public void ExtractArchive(string archivePath, string extractPath) { using var archive = ZipFile.OpenRead(archivePath); ExtractArchiveEntries(archive, extractPath); + _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.SharpCompress: @@ -568,6 +571,7 @@ public void ExtractArchive(string archivePath, string extractPath) ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)), extractPath); + _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.NotSupported: diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs new file mode 100644 index 0000000000..866587d6ac --- /dev/null +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Threading; +using Kavita.Common.EnvironmentInfo; + +namespace API.Services.ImageConversion; + +public interface IImageConverterProvider +{ + string Extension { get; } + bool IsSupported(string filename); + string Convert(string filename); +} + +public class JpegXLImageConverterProvider : IImageConverterProvider +{ + public bool IsSupported(string filename) => filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); + public string Extension => ".jxl"; + public string Convert(string filename) + { + string exe = "djxl"; + if (OsInfo.IsWindows) + exe = "djxl.exe"; + string destination = Path.ChangeExtension(filename, "jpg"); + OsInfo.RunAndCapture(exe, "\"" + filename + "\" \"" + destination + "\"", Timeout.Infinite); + File.Delete(filename); + return destination; + } +} diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs new file mode 100644 index 0000000000..08cab27a87 --- /dev/null +++ b/API/Services/ImageConverterService.cs @@ -0,0 +1,62 @@ +using NetVips; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using API.Services.ImageConversion; +using static Org.BouncyCastle.Bcpg.Attr.ImageAttrib; + +namespace API.Services +{ + public interface IImageConverterService + { + Stream ConvertStream(string filename, Stream source); + string ConvertFile(string filename); + void ConvertDirectory(string directory); + } + + public class ImageConverterService : IImageConverterService + { + private IEnumerable _converters; + private readonly IDirectoryService _directoryService; + public ImageConverterService(IDirectoryService directoryService, IEnumerable converters) + { + _directoryService = directoryService; + _converters = converters; + } + + + + public Stream ConvertStream(string filename, Stream source) + { + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); + if (provider == null) + return source; + string tempFile = Path.ChangeExtension(Path.GetFileName(filename), provider.Extension); + Stream dest = File.OpenWrite(tempFile); + source.CopyTo(dest); + source.Close(); + dest.Close(); + return File.OpenRead(provider.Convert(tempFile)); + } + + public string ConvertFile(string filename) + { + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); + if (provider == null) + return filename; + return provider.Convert(filename); + } + + public void ConvertDirectory(string directory) + { + foreach (string filename in Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories)) + { + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); + if (provider != null) + provider.Convert(filename); + } + } + } +} diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index ad6829b7dd..9dc31038d0 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; @@ -76,6 +76,7 @@ public class ImageService : IImageService private readonly ILogger _logger; private readonly IDirectoryService _directoryService; private readonly IEasyCachingProviderFactory _cacheFactory; + private readonly IImageConverterService _converterService; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"series\d+"; public const string CollectionTagCoverImageRegex = @"tag\d+"; @@ -114,11 +115,12 @@ public class ImageService : IImageService ["https://app.plex.tv"] = "https://plex.tv" }; - public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory) + public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory, IImageConverterService converterService) { _logger = logger; _directoryService = directoryService; _cacheFactory = cacheFactory; + _converterService = converterService; } public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) @@ -134,6 +136,7 @@ public void ExtractImages(string? fileFilePath, string targetDirectory, int file _directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, Tasks.Scanner.Parser.Parser.ImageFileExtensions); } + _converterService.ConvertDirectory(targetDirectory); } /// @@ -214,6 +217,7 @@ public string GetCoverImage(string path, string fileName, string outputDirectory try { + fileName = _converterService.ConvertFile(fileName); var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 840e7a6d81..78f784cdd6 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -25,7 +25,7 @@ public static class Parser public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); - public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser + public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif|\.jxl)"; // Don't forget to update CoverChooser public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; public const string EpubFileExtension = @"\.epub"; public const string PdfFileExtension = @"\.pdf"; diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs index d8cc6a070e..6b99e0eac1 100644 --- a/Kavita.Common/EnvironmentInfo/IOsInfo.cs +++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; namespace Kavita.Common.EnvironmentInfo; @@ -57,7 +57,7 @@ private static Os GetPosixFlavour() } } - private static string RunAndCapture(string filename, string args) + public static string RunAndCapture(string filename, string args, int waitInMilliseconds = 1000) { var p = new Process { @@ -75,7 +75,7 @@ private static string RunAndCapture(string filename, string args) // To avoid deadlocks, always read the output stream first and then wait. var output = p.StandardOutput.ReadToEnd(); - p.WaitForExit(1000); + p.WaitForExit(waitInMilliseconds); return output; } From cb647ad79086cade5f57446334505491ac9c06eb Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 15 Aug 2024 17:40:47 -0300 Subject: [PATCH 02/37] Make sure if djxl do not exists, continue as usual. Fixed test and benchmark DI. Point docker-build to another hub Edit dockerfile and include jxl package. --- API.Benchmark/ArchiveServiceBenchmark.cs | 6 +- API.Tests/Services/ArchiveServiceTests.cs | 12 +- API.Tests/Services/BookServiceTests.cs | 4 +- .../JpegXLImageConverterProvider.cs | 38 ++++++- Dockerfile | 6 +- docker-build-jxl.sh | 103 ++++++++++++++++++ 6 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 docker-build-jxl.sh diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index 9ef8e237bc..79ef38b2e9 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using Microsoft.Extensions.Logging.Abstractions; @@ -32,8 +32,8 @@ public class ArchiveServiceBenchmark public ArchiveServiceBenchmark() { _directoryService = new DirectoryService(null, new FileSystem()); - _imageService = new ImageService(null, _directoryService, Substitute.For()); - _archiveService = new ArchiveService(new NullLogger(), _directoryService, _imageService, Substitute.For()); + _imageService = new ImageService(null, _directoryService, Substitute.For(), Substitute.For()); + _archiveService = new ArchiveService(new NullLogger(), _directoryService, _imageService, Substitute.For(), Substitute.For()); } [Benchmark(Baseline = true)] diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 086d998634..27a93ea649 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; @@ -29,8 +29,8 @@ public ArchiveServiceTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _archiveService = new ArchiveService(_logger, _directoryService, - new ImageService(Substitute.For>(), _directoryService, Substitute.For()), - Substitute.For()); + new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()), + Substitute.For(), Substitute.For()); } [Theory] @@ -167,7 +167,7 @@ public void FindFirstEntry(string[] files, string expected) public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile) { var ds = Substitute.For(_directoryServiceLogger, new FileSystem()); - var imageService = new ImageService(Substitute.For>(), ds, Substitute.For()); + var imageService = new ImageService(Substitute.For>(), ds, Substitute.For(), Substitute.For()); var archiveService = Substitute.For(_logger, ds, imageService, Substitute.For()); var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")); @@ -198,7 +198,7 @@ public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFi [InlineData("sorting.zip", "sorting.expected.png")] public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile) { - var imageService = new ImageService(Substitute.For>(), _directoryService, Substitute.For()); + var imageService = new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()); var archiveService = Substitute.For(_logger, new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService, Substitute.For()); @@ -226,7 +226,7 @@ public void CanParseCoverImage(string inputFile) var imageService = Substitute.For(); imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); - var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); + var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For(),Substitute.For()); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile)); var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output"); diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index e4647524ef..2fc1e18d12 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.IO.Abstractions; using API.Services; using EasyCaching.Core; @@ -17,7 +17,7 @@ public BookServiceTests() { var directoryService = new DirectoryService(Substitute.For>(), new FileSystem()); _bookService = new BookService(_logger, directoryService, - new ImageService(Substitute.For>(), directoryService, Substitute.For()) + new ImageService(Substitute.For>(), directoryService, Substitute.For(), Substitute.For()) , Substitute.For()); } diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 866587d6ac..95b399afd2 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -14,15 +14,43 @@ public interface IImageConverterProvider public class JpegXLImageConverterProvider : IImageConverterProvider { - public bool IsSupported(string filename) => filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); + private bool? _appFound = null; + + internal bool AppFound + { + get + { + if (_appFound == null) + { + try + { + _appFound = OsInfo.RunAndCapture(exeFile, "--version", Timeout.Infinite).Contains("JPEG XL"); + } + catch (Exception e) + { + //Eat it + _appFound = false; + } + } + return _appFound.Value; + } + } + private string exeFile => OsInfo.IsWindows ? "djxl.exe" : "djxl"; + + public bool IsSupported(string filename) + { + if (AppFound) + return filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); + return false; + } + public string Extension => ".jxl"; public string Convert(string filename) { - string exe = "djxl"; - if (OsInfo.IsWindows) - exe = "djxl.exe"; + if (!AppFound) + return filename; string destination = Path.ChangeExtension(filename, "jpg"); - OsInfo.RunAndCapture(exe, "\"" + filename + "\" \"" + destination + "\"", Timeout.Infinite); + OsInfo.RunAndCapture(exeFile, "\"" + filename + "\" \"" + destination + "\"", Timeout.Infinite); File.Delete(filename); return destination; } diff --git a/Dockerfile b/Dockerfile index 6d52acaba3..3eb3417c57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ #This Dockerfile creates a build for all architectures #Image that copies in the files and passes them to the main image -FROM ubuntu:focal AS copytask +FROM ubuntu:oracular AS copytask ARG TARGETPLATFORM @@ -14,7 +14,7 @@ RUN /copy_runtime.sh RUN chmod +x /Kavita/Kavita #Production image -FROM ubuntu:focal +FROM ubuntu:oracular COPY --from=copytask /Kavita /kavita COPY --from=copytask /files/wwwroot /kavita/wwwroot @@ -22,7 +22,7 @@ COPY API/config/appsettings.json /tmp/config/appsettings.json #Installs program dependencies RUN apt-get update \ - && apt-get install -y libicu-dev libssl1.1 libgdiplus curl \ + && apt-get install -y libicu-dev libssl3t64 libgdiplus curl libjxl-tools \ && rm -rf /var/lib/apt/lists/* COPY entrypoint.sh /entrypoint.sh diff --git a/docker-build-jxl.sh b/docker-build-jxl.sh new file mode 100644 index 0000000000..206b97f0b6 --- /dev/null +++ b/docker-build-jxl.sh @@ -0,0 +1,103 @@ +#! /bin/bash +set -e + +outputFolder='_output' + +ProgressStart() +{ + echo "Start '$1'" +} + +ProgressEnd() +{ + echo "Finish '$1'" +} + +Build() +{ + local RID="$1" + + ProgressStart "Build for $RID" + + slnFile=Kavita.sln + + dotnet clean $slnFile -c Release + + dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID + + ProgressEnd "Build for $RID" +} + +BuildUI() +{ + ProgressStart 'Building UI' + echo 'Removing old wwwroot' + rm -rf API/wwwroot/* + cd UI/Web/ || exit + echo 'Installing web dependencies' + npm install --legacy-peer-deps + echo 'Building UI' + npm run prod + echo 'Copying back to Kavita wwwroot' + mkdir -p ../../API/wwwroot + cp -R dist/browser/* ../../API/wwwroot + cd ../../ || exit + ProgressEnd 'Building UI' +} + +Package() +{ + local runtime="$1" + local lOutputFolder=../_output/"$runtime"/Kavita + + ProgressStart "Creating $runtime Package" + + # TODO: Use no-restore? Because Build should have already done it for us + echo "Building" + cd API + echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" + dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" + + echo "Copying Install information" + cp ../INSTALL.txt "$lOutputFolder"/README.txt + + echo "Copying LICENSE" + cp ../LICENSE "$lOutputFolder"/LICENSE.txt + + echo "Renaming API -> Kavita" + mv "$lOutputFolder"/API "$lOutputFolder"/Kavita + + echo "Creating tar" + cd ../$outputFolder/"$runtime"/ + tar -czvf ../kavita-$runtime.tar.gz Kavita + + ProgressEnd "Creating $runtime Package" + +} + +dir=$PWD + +if [ -d _output ] +then + rm -r _output/ +fi + +BuildUI + +#Build for x64 +Build "linux-x64" +Package "linux-x64" +cd "$dir" + +#Build for arm +Build "linux-arm" +Package "linux-arm" +cd "$dir" + +#Build for arm64 +Build "linux-arm64" +Package "linux-arm64" +cd "$dir" + +#Builds Docker images +docker buildx build -t maxpiva/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push From 7f6db2439cfe00e6051ea3237b621ed0ec64054f Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 23 Aug 2024 18:34:58 -0300 Subject: [PATCH 03/37] Changed the way conversion happens. 1. Supported Image Formats came in the accept flag from the browser. 2. If jxl (or future formats, ie browser not supported AVIF, HEIF, etc) are not supported, Kavita will convert it to a suitable display format via conversion providers. 3. if it supported it will return the image as is, no conversion needed. 4. Thumbnails are always converted. --- API.Tests/Services/ArchiveServiceTests.cs | 6 ++-- API.Tests/Services/CacheServiceTests.cs | 16 ++++----- API/API.csproj | 3 +- API/Controllers/OPDSController.cs | 4 +-- API/Controllers/ReaderController.cs | 13 ++++--- API/Extensions/FileTypeGroupExtensions.cs | 12 ++++++- API/Extensions/HttpExtensions.cs | 32 ++++++++++++++++- API/Services/ArchiveService.cs | 15 ++++---- API/Services/BookService.cs | 6 ++-- API/Services/CacheService.cs | 36 ++++++++++++------- .../JpegXLImageConverterProvider.cs | 16 +++++++++ API/Services/ImageConverterService.cs | 24 +++++++++++-- API/Services/ImageService.cs | 9 +++-- 13 files changed, 140 insertions(+), 52 deletions(-) diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 27a93ea649..530affbf31 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -30,7 +30,7 @@ public ArchiveServiceTests(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; _archiveService = new ArchiveService(_logger, _directoryService, new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()), - Substitute.For(), Substitute.For()); + Substitute.For()); } [Theory] @@ -224,9 +224,9 @@ public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOu public void CanParseCoverImage(string inputFile) { var imageService = Substitute.For(); - imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); - var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For(),Substitute.For()); + var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile)); var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output"); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index ba06525a35..c5ae07bee4 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; @@ -158,7 +158,7 @@ public async Task Ensure_DirectoryAlreadyExists_DontExtractAnything() new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); await ResetDB(); var s = new SeriesBuilder("Test").Build(); @@ -234,7 +234,7 @@ public void CleanupChapters_AllFilesShouldBeDeleted() var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); @@ -256,7 +256,7 @@ public void GetCachedEpubFile_ShouldReturnFirstEpub() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); var c = new ChapterBuilder("1") .WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build()) @@ -297,7 +297,7 @@ public void GetCachedPagePath_ReturnNullIfNoFiles() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(),Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -341,7 +341,7 @@ public void GetCachedPagePath_GetFileFromFirstFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -382,7 +382,7 @@ public void GetCachedPagePath_GetLastPageFromSingleFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -427,7 +427,7 @@ public void GetCachedPagePath_GetFileFromSecondFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); diff --git a/API/API.csproj b/API/API.csproj index d257052692..ea886732b9 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -13,8 +13,7 @@ - - + diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 6d0f7e8dde..2df6e5ea70 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1227,7 +1227,7 @@ public async Task GetPageStreamedImage(string apiKey, [FromQuery] try { - var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); + var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber, Request.SupportedImageTypesFromRequest()); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); @@ -1251,7 +1251,7 @@ await _readerService.SaveReadingProgress(new ProgressDto() }, userId); } - return File(content, MimeTypeMap.GetMimeType(format)); + return File(content, format.GetMimeType()); } catch (Exception) { diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 159c8c922b..99d096f749 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -23,6 +23,7 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using MimeTypes; +using Org.BouncyCastle.Ocsp; namespace API.Controllers; @@ -118,12 +119,12 @@ public async Task GetImage(int chapterId, int page, string apiKey, var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); _logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId); - var path = _cacheService.GetCachedPagePath(chapter.Id, page); + var path = _cacheService.GetCachedPagePath(chapter.Id, page, Request.SupportedImageTypesFromRequest()); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); - return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true); + return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path), true); } catch (Exception) { @@ -132,6 +133,8 @@ public async Task GetImage(int chapterId, int page, string apiKey, } } + + /// /// Returns a thumbnail for the given page number /// @@ -180,11 +183,11 @@ public async Task GetBookmarkImage(int seriesId, string apiKey, in try { - var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); + var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page, Request.SupportedImageTypesFromRequest()); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); - return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path)); + return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path)); } catch (Exception) { @@ -729,7 +732,7 @@ public async Task BookmarkPage(BookmarkDto bookmarkDto) if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find")); bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); - var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); + var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page, Request.SupportedImageTypesFromRequest()); if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save")); diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index 24073f6424..9cbd330980 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -1,6 +1,7 @@ -using System; +using System; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; +using MimeTypes; namespace API.Extensions; @@ -22,4 +23,13 @@ public static string GetRegex(this FileTypeGroup fileTypeGroup) throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); } } + public static string GetMimeType(this string format) + { + //Add jxl format + format = format.ToLowerInvariant(); + if (format == ".jxl") + return "image/jxl"; + return MimeTypeMap.GetMimeType(format); + } + } diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index fbf8281044..6287f31f89 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,5 +1,8 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -53,4 +56,31 @@ public static void AddCacheHeader(this HttpResponse response, string filename, i response.Headers.CacheControl = $"max-age={maxAge}"; } } + + public static List SupportedImageTypesFromRequest(this HttpRequest request) + { + var acceptHeader = request.Headers["Accept"]; + string[] spl1 = acceptHeader.ToString().Split(';'); + acceptHeader = spl1[0]; + string[] split = acceptHeader.ToString().Split(','); + List defaultExtensions = new List(); + defaultExtensions.Add("jpeg"); + defaultExtensions.Add("jpg"); + defaultExtensions.Add("png"); + defaultExtensions.Add("gif"); + + foreach (string v in split) + { + if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) + { + string n = v.Substring(6); + if (n == "svg+xml") + n = "svg"; + if (n.StartsWith("*")) + continue; + defaultExtensions.Add(n.ToLowerInvariant()); + } + } + return defaultExtensions; + } } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 8a42844020..641c25c0d3 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -58,17 +58,16 @@ public class ArchiveService : IArchiveService private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly IMediaErrorService _mediaErrorService; - private readonly IImageConverterService _converterService; + private const string ComicInfoFilename = "ComicInfo.xml"; public ArchiveService(ILogger logger, IDirectoryService directoryService, - IImageService imageService, IMediaErrorService mediaErrorService, IImageConverterService converterService) + IImageService imageService, IMediaErrorService mediaErrorService) { _logger = logger; _directoryService = directoryService; _imageService = imageService; _mediaErrorService = mediaErrorService; - _converterService = converterService; } /// @@ -233,8 +232,8 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, archive.Entries.Select(e => e.FullName)); var entry = archive.Entries.Single(e => e.FullName == entryName); - using var stream = _converterService.ConvertStream(entryName, entry.Open()); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + using var stream = entry.Open(); + return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: { @@ -244,8 +243,8 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, entryNames); var entry = archive.Entries.Single(e => e.Key == entryName); - using var stream = _converterService.ConvertStream(entryName, entry.OpenEntryStream()); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + using var stream = entry.OpenEntryStream(); + return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); @@ -562,7 +561,6 @@ public void ExtractArchive(string archivePath, string extractPath) { using var archive = ZipFile.OpenRead(archivePath); ExtractArchiveEntries(archive, extractPath); - _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.SharpCompress: @@ -571,7 +569,6 @@ public void ExtractArchive(string archivePath, string extractPath) ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)), extractPath); - _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.NotSupported: diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index e4ed920473..cb22f24d66 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -1229,7 +1229,7 @@ public string GetCoverImage(string fileFilePath, string fileName, string outputD if (coverImageContent == null) return string.Empty; using var stream = coverImageContent.GetContentStream(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { @@ -1252,7 +1252,7 @@ private string GetPdfCoverImage(string fileFilePath, string fileName, string out using var stream = StreamManager.GetStream("BookService.GetPdfPage"); GetPdfPage(docReader, 0, stream); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index c6e5393486..a00df5879f 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -13,6 +13,7 @@ using API.Extensions; using Kavita.Common; using Microsoft.Extensions.Logging; +using MimeKit.Tnef; using NetVips; namespace API.Services; @@ -34,12 +35,12 @@ public interface ICacheService /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. void CleanupChapters(IEnumerable chapterIds); void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(int chapterId, int page); + string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null); string GetCachePath(int chapterId); string GetBookmarkCachePath(int seriesId); IEnumerable GetCachedPages(int chapterId); IEnumerable GetCachedFileDimensions(string cachePath); - string GetCachedBookmarkPagePath(int seriesId, int page); + string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null); string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); Task CacheBookmarkForSeries(int userId, int seriesId); @@ -52,18 +53,19 @@ public class CacheService : ICacheService private readonly IDirectoryService _directoryService; private readonly IReadingItemService _readingItemService; private readonly IBookmarkService _bookmarkService; - + private readonly IImageConverterService _converterService; private static readonly ConcurrentDictionary ExtractLocks = new(); public CacheService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IReadingItemService readingItemService, - IBookmarkService bookmarkService) + IBookmarkService bookmarkService, IImageConverterService converterService) { _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; _readingItemService = readingItemService; _bookmarkService = bookmarkService; + _converterService = converterService; } public IEnumerable GetCachedPages(int chapterId) @@ -98,13 +100,19 @@ public IEnumerable GetCachedFileDimensions(string cachePath) for (var i = 0; i < files.Length; i++) { var file = files[i]; - using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); + var dimension = _converterService.GetDimensions(file, i); + if (dimension == null) + { + using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); + dimension = (image.Width, image.Height); + } + dimensions.Add(new FileDimensionDto() { PageNumber = i, - Height = image.Height, - Width = image.Width, - IsWide = image.Width > image.Height, + Height = dimension.Value.Height, + Width = dimension.Value.Width, + IsWide = dimension.Value.Width > dimension.Value.Height, FileName = file.Replace(cachePath, string.Empty) }); } @@ -122,7 +130,7 @@ public IEnumerable GetCachedFileDimensions(string cachePath) return dimensions; } - public string GetCachedBookmarkPagePath(int seriesId, int page) + public string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null) { // Calculate what chapter the page belongs to var path = GetBookmarkCachePath(seriesId); @@ -138,7 +146,8 @@ public string GetCachedBookmarkPagePath(int seriesId, int page) } // Since array is 0 based, we need to keep that in account (only affects last image) - return page == files.Length ? files[page - 1] : files[page]; + string file=page == files.Length ? files[page - 1] : files[page]; + return _converterService.ConvertFile(file, supportedImageFormats); } /// @@ -316,7 +325,7 @@ public string GetBookmarkCachePath(int seriesId) /// Chapter id with Files populated. /// Page number to look for /// Page filepath or empty if no files found. - public string GetCachedPagePath(int chapterId, int page) + public string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null) { // Calculate what chapter the page belongs to var path = GetCachePath(chapterId); @@ -325,7 +334,8 @@ public string GetCachedPagePath(int chapterId, int page) //.OrderByNatural(Path.GetFileNameWithoutExtension) // This is already done in GetPageFromFiles .ToArray(); - return GetPageFromFiles(files, page); + string file = GetPageFromFiles(files, page); + return _converterService.ConvertFile(file, supportedImageFormats); } public async Task CacheBookmarkForSeries(int userId, int seriesId) diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 95b399afd2..3bd1a2f113 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using System.Threading; using Kavita.Common.EnvironmentInfo; @@ -10,6 +11,7 @@ public interface IImageConverterProvider string Extension { get; } bool IsSupported(string filename); string Convert(string filename); + (int Width, int Height)? GetDimensions(string fileName, int pageNumber); } public class JpegXLImageConverterProvider : IImageConverterProvider @@ -37,6 +39,8 @@ internal bool AppFound } private string exeFile => OsInfo.IsWindows ? "djxl.exe" : "djxl"; + private string infoFile => OsInfo.IsWindows ? "jxlinfo.exe" : "jxlinfo"; + public bool IsSupported(string filename) { if (AppFound) @@ -44,6 +48,18 @@ public bool IsSupported(string filename) return false; } + private static Regex dimensions = new Regex(@",\s?(\d+)x(\d+),", RegexOptions.Compiled); + public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) + { + if (!AppFound) + return null; + string output = OsInfo.RunAndCapture(infoFile, "\"" + fileName + "\"", Timeout.Infinite); + Match m= dimensions.Match(output); + if (!m.Success) + return null; + return (int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value)); + } + public string Extension => ".jxl"; public string Convert(string filename) { diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index 08cab27a87..e2f61dd7a6 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -6,14 +6,16 @@ using System.Linq; using API.Services.ImageConversion; using static Org.BouncyCastle.Bcpg.Attr.ImageAttrib; +using API.DTOs.Reader; namespace API.Services { public interface IImageConverterService { Stream ConvertStream(string filename, Stream source); - string ConvertFile(string filename); + string ConvertFile(string filename, List supportedImageFormats); void ConvertDirectory(string directory); + (int Width, int Height)? GetDimensions(string fileName, int pageNumber); } public class ImageConverterService : IImageConverterService @@ -41,8 +43,18 @@ public Stream ConvertStream(string filename, Stream source) return File.OpenRead(provider.Convert(tempFile)); } - public string ConvertFile(string filename) + private bool CheckDirectSupport(string filename, List supportedImageFormats) { + if (supportedImageFormats == null) + return false; + string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); + return supportedImageFormats.Contains(ext); + } + + public string ConvertFile(string filename, List supportedImageFormats) + { + if (CheckDirectSupport(filename, supportedImageFormats)) + return filename; IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); if (provider == null) return filename; @@ -58,5 +70,13 @@ public void ConvertDirectory(string directory) provider.Convert(filename); } } + + public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) + { + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); + if (provider == null) + return null; + return provider.GetDimensions(fileName, pageNumber); + } } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 9dc31038d0..2656c8446f 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -47,7 +47,7 @@ public interface IImageService /// /// /// - string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); /// /// Writes out a thumbnail by file path input /// @@ -217,7 +217,7 @@ public string GetCoverImage(string path, string fileName, string outputDirectory try { - fileName = _converterService.ConvertFile(fileName); + fileName = _converterService.ConvertFile(fileName, null); var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); @@ -245,8 +245,9 @@ public string GetCoverImage(string path, string fileName, string outputDirectory /// Where to output the file, defaults to covers directory /// Export the file as the passed encoding /// File name with extension of the file. This will always write to - public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) + public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { + stream = _converterService.ConvertStream(sourceFile, stream); var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; using var sourceImage = Image.NewFromStream(stream); @@ -289,6 +290,8 @@ public string WriteCoverThumbnail(Stream stream, string fileName, string outputD public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { + sourceFile = _converterService.ConvertFile(sourceFile,null); + var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); From dd0021a7566fe08626bbf569c964e32388a88fb6 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 23 Aug 2024 18:35:37 -0300 Subject: [PATCH 04/37] Missing file --- API.Benchmark/ArchiveServiceBenchmark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index 79ef38b2e9..fd82ee87d4 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -33,7 +33,7 @@ public ArchiveServiceBenchmark() { _directoryService = new DirectoryService(null, new FileSystem()); _imageService = new ImageService(null, _directoryService, Substitute.For(), Substitute.For()); - _archiveService = new ArchiveService(new NullLogger(), _directoryService, _imageService, Substitute.For(), Substitute.For()); + _archiveService = new ArchiveService(new NullLogger(), _directoryService, _imageService, Substitute.For()); } [Benchmark(Baseline = true)] From 9d48664b08f01303f1527bc1c577e1d4376f89ff Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Mon, 26 Aug 2024 00:22:34 -0300 Subject: [PATCH 05/37] Revert "Changed the way conversion happens." This reverts commit 7f6db2439cfe00e6051ea3237b621ed0ec64054f. --- API.Tests/Services/ArchiveServiceTests.cs | 6 ++-- API.Tests/Services/CacheServiceTests.cs | 16 ++++----- API/API.csproj | 3 +- API/Controllers/OPDSController.cs | 4 +-- API/Controllers/ReaderController.cs | 13 +++---- API/Extensions/FileTypeGroupExtensions.cs | 12 +------ API/Extensions/HttpExtensions.cs | 32 +---------------- API/Services/ArchiveService.cs | 15 ++++---- API/Services/BookService.cs | 6 ++-- API/Services/CacheService.cs | 36 +++++++------------ .../JpegXLImageConverterProvider.cs | 16 --------- API/Services/ImageConverterService.cs | 24 ++----------- API/Services/ImageService.cs | 9 ++--- 13 files changed, 52 insertions(+), 140 deletions(-) diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 530affbf31..27a93ea649 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -30,7 +30,7 @@ public ArchiveServiceTests(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; _archiveService = new ArchiveService(_logger, _directoryService, new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()), - Substitute.For()); + Substitute.For(), Substitute.For()); } [Theory] @@ -224,9 +224,9 @@ public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOu public void CanParseCoverImage(string inputFile) { var imageService = Substitute.For(); - imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); - var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); + var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For(),Substitute.For()); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile)); var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output"); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index c5ae07bee4..ba06525a35 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; @@ -158,7 +158,7 @@ public async Task Ensure_DirectoryAlreadyExists_DontExtractAnything() new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For()); await ResetDB(); var s = new SeriesBuilder("Test").Build(); @@ -234,7 +234,7 @@ public void CleanupChapters_AllFilesShouldBeDeleted() var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); @@ -256,7 +256,7 @@ public void GetCachedEpubFile_ShouldReturnFirstEpub() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For()); var c = new ChapterBuilder("1") .WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build()) @@ -297,7 +297,7 @@ public void GetCachedPagePath_ReturnNullIfNoFiles() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(),Substitute.For()); + Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -341,7 +341,7 @@ public void GetCachedPagePath_GetFileFromFirstFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -382,7 +382,7 @@ public void GetCachedPagePath_GetLastPageFromSingleFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -427,7 +427,7 @@ public void GetCachedPagePath_GetFileFromSecondFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); diff --git a/API/API.csproj b/API/API.csproj index 1755146ff9..9cdecc8d1b 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -13,7 +13,8 @@ - + + diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 0b260a5379..d25321ad8a 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1228,7 +1228,7 @@ public async Task GetPageStreamedImage(string apiKey, [FromQuery] try { - var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); @@ -1252,7 +1252,7 @@ await _readerService.SaveReadingProgress(new ProgressDto() }, userId); } - return File(content, format.GetMimeType()); + return File(content, MimeTypeMap.GetMimeType(format)); } catch (Exception) { diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 99d096f749..159c8c922b 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -23,7 +23,6 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using MimeTypes; -using Org.BouncyCastle.Ocsp; namespace API.Controllers; @@ -119,12 +118,12 @@ public async Task GetImage(int chapterId, int page, string apiKey, var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); _logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId); - var path = _cacheService.GetCachedPagePath(chapter.Id, page, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); - return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path), true); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true); } catch (Exception) { @@ -133,8 +132,6 @@ public async Task GetImage(int chapterId, int page, string apiKey, } } - - /// /// Returns a thumbnail for the given page number /// @@ -183,11 +180,11 @@ public async Task GetBookmarkImage(int seriesId, string apiKey, in try { - var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); - return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path)); } catch (Exception) { @@ -732,7 +729,7 @@ public async Task BookmarkPage(BookmarkDto bookmarkDto) if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find")); bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); - var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save")); diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index 9cbd330980..24073f6424 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -1,7 +1,6 @@ -using System; +using System; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; -using MimeTypes; namespace API.Extensions; @@ -23,13 +22,4 @@ public static string GetRegex(this FileTypeGroup fileTypeGroup) throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); } } - public static string GetMimeType(this string format) - { - //Add jxl format - format = format.ToLowerInvariant(); - if (format == ".jxl") - return "image/jxl"; - return MimeTypeMap.GetMimeType(format); - } - } diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 6287f31f89..fbf8281044 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.IO; using System.Linq; -using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -56,31 +53,4 @@ public static void AddCacheHeader(this HttpResponse response, string filename, i response.Headers.CacheControl = $"max-age={maxAge}"; } } - - public static List SupportedImageTypesFromRequest(this HttpRequest request) - { - var acceptHeader = request.Headers["Accept"]; - string[] spl1 = acceptHeader.ToString().Split(';'); - acceptHeader = spl1[0]; - string[] split = acceptHeader.ToString().Split(','); - List defaultExtensions = new List(); - defaultExtensions.Add("jpeg"); - defaultExtensions.Add("jpg"); - defaultExtensions.Add("png"); - defaultExtensions.Add("gif"); - - foreach (string v in split) - { - if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) - { - string n = v.Substring(6); - if (n == "svg+xml") - n = "svg"; - if (n.StartsWith("*")) - continue; - defaultExtensions.Add(n.ToLowerInvariant()); - } - } - return defaultExtensions; - } } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 641c25c0d3..8a42844020 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -58,16 +58,17 @@ public class ArchiveService : IArchiveService private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly IMediaErrorService _mediaErrorService; - + private readonly IImageConverterService _converterService; private const string ComicInfoFilename = "ComicInfo.xml"; public ArchiveService(ILogger logger, IDirectoryService directoryService, - IImageService imageService, IMediaErrorService mediaErrorService) + IImageService imageService, IMediaErrorService mediaErrorService, IImageConverterService converterService) { _logger = logger; _directoryService = directoryService; _imageService = imageService; _mediaErrorService = mediaErrorService; + _converterService = converterService; } /// @@ -232,8 +233,8 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, archive.Entries.Select(e => e.FullName)); var entry = archive.Entries.Single(e => e.FullName == entryName); - using var stream = entry.Open(); - return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); + using var stream = _converterService.ConvertStream(entryName, entry.Open()); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: { @@ -243,8 +244,8 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, entryNames); var entry = archive.Entries.Single(e => e.Key == entryName); - using var stream = entry.OpenEntryStream(); - return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); + using var stream = _converterService.ConvertStream(entryName, entry.OpenEntryStream()); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); @@ -561,6 +562,7 @@ public void ExtractArchive(string archivePath, string extractPath) { using var archive = ZipFile.OpenRead(archivePath); ExtractArchiveEntries(archive, extractPath); + _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.SharpCompress: @@ -569,6 +571,7 @@ public void ExtractArchive(string archivePath, string extractPath) ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)), extractPath); + _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.NotSupported: diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index cb22f24d66..e4ed920473 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -1229,7 +1229,7 @@ public string GetCoverImage(string fileFilePath, string fileName, string outputD if (coverImageContent == null) return string.Empty; using var stream = coverImageContent.GetContentStream(); - return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { @@ -1252,7 +1252,7 @@ private string GetPdfCoverImage(string fileFilePath, string fileName, string out using var stream = StreamManager.GetStream("BookService.GetPdfPage"); GetPdfPage(docReader, 0, stream); - return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index a00df5879f..c6e5393486 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -13,7 +13,6 @@ using API.Extensions; using Kavita.Common; using Microsoft.Extensions.Logging; -using MimeKit.Tnef; using NetVips; namespace API.Services; @@ -35,12 +34,12 @@ public interface ICacheService /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. void CleanupChapters(IEnumerable chapterIds); void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null); + string GetCachedPagePath(int chapterId, int page); string GetCachePath(int chapterId); string GetBookmarkCachePath(int seriesId); IEnumerable GetCachedPages(int chapterId); IEnumerable GetCachedFileDimensions(string cachePath); - string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null); + string GetCachedBookmarkPagePath(int seriesId, int page); string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); Task CacheBookmarkForSeries(int userId, int seriesId); @@ -53,19 +52,18 @@ public class CacheService : ICacheService private readonly IDirectoryService _directoryService; private readonly IReadingItemService _readingItemService; private readonly IBookmarkService _bookmarkService; - private readonly IImageConverterService _converterService; + private static readonly ConcurrentDictionary ExtractLocks = new(); public CacheService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IReadingItemService readingItemService, - IBookmarkService bookmarkService, IImageConverterService converterService) + IBookmarkService bookmarkService) { _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; _readingItemService = readingItemService; _bookmarkService = bookmarkService; - _converterService = converterService; } public IEnumerable GetCachedPages(int chapterId) @@ -100,19 +98,13 @@ public IEnumerable GetCachedFileDimensions(string cachePath) for (var i = 0; i < files.Length; i++) { var file = files[i]; - var dimension = _converterService.GetDimensions(file, i); - if (dimension == null) - { - using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); - dimension = (image.Width, image.Height); - } - + using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); dimensions.Add(new FileDimensionDto() { PageNumber = i, - Height = dimension.Value.Height, - Width = dimension.Value.Width, - IsWide = dimension.Value.Width > dimension.Value.Height, + Height = image.Height, + Width = image.Width, + IsWide = image.Width > image.Height, FileName = file.Replace(cachePath, string.Empty) }); } @@ -130,7 +122,7 @@ public IEnumerable GetCachedFileDimensions(string cachePath) return dimensions; } - public string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null) + public string GetCachedBookmarkPagePath(int seriesId, int page) { // Calculate what chapter the page belongs to var path = GetBookmarkCachePath(seriesId); @@ -146,8 +138,7 @@ public string GetCachedBookmarkPagePath(int seriesId, int page, List sup } // Since array is 0 based, we need to keep that in account (only affects last image) - string file=page == files.Length ? files[page - 1] : files[page]; - return _converterService.ConvertFile(file, supportedImageFormats); + return page == files.Length ? files[page - 1] : files[page]; } /// @@ -325,7 +316,7 @@ public string GetBookmarkCachePath(int seriesId) /// Chapter id with Files populated. /// Page number to look for /// Page filepath or empty if no files found. - public string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null) + public string GetCachedPagePath(int chapterId, int page) { // Calculate what chapter the page belongs to var path = GetCachePath(chapterId); @@ -334,8 +325,7 @@ public string GetCachedPagePath(int chapterId, int page, List supportedI //.OrderByNatural(Path.GetFileNameWithoutExtension) // This is already done in GetPageFromFiles .ToArray(); - string file = GetPageFromFiles(files, page); - return _converterService.ConvertFile(file, supportedImageFormats); + return GetPageFromFiles(files, page); } public async Task CacheBookmarkForSeries(int userId, int seriesId) diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 3bd1a2f113..95b399afd2 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Text.RegularExpressions; using System.Threading; using Kavita.Common.EnvironmentInfo; @@ -11,7 +10,6 @@ public interface IImageConverterProvider string Extension { get; } bool IsSupported(string filename); string Convert(string filename); - (int Width, int Height)? GetDimensions(string fileName, int pageNumber); } public class JpegXLImageConverterProvider : IImageConverterProvider @@ -39,8 +37,6 @@ internal bool AppFound } private string exeFile => OsInfo.IsWindows ? "djxl.exe" : "djxl"; - private string infoFile => OsInfo.IsWindows ? "jxlinfo.exe" : "jxlinfo"; - public bool IsSupported(string filename) { if (AppFound) @@ -48,18 +44,6 @@ public bool IsSupported(string filename) return false; } - private static Regex dimensions = new Regex(@",\s?(\d+)x(\d+),", RegexOptions.Compiled); - public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) - { - if (!AppFound) - return null; - string output = OsInfo.RunAndCapture(infoFile, "\"" + fileName + "\"", Timeout.Infinite); - Match m= dimensions.Match(output); - if (!m.Success) - return null; - return (int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value)); - } - public string Extension => ".jxl"; public string Convert(string filename) { diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index e2f61dd7a6..08cab27a87 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -6,16 +6,14 @@ using System.Linq; using API.Services.ImageConversion; using static Org.BouncyCastle.Bcpg.Attr.ImageAttrib; -using API.DTOs.Reader; namespace API.Services { public interface IImageConverterService { Stream ConvertStream(string filename, Stream source); - string ConvertFile(string filename, List supportedImageFormats); + string ConvertFile(string filename); void ConvertDirectory(string directory); - (int Width, int Height)? GetDimensions(string fileName, int pageNumber); } public class ImageConverterService : IImageConverterService @@ -43,18 +41,8 @@ public Stream ConvertStream(string filename, Stream source) return File.OpenRead(provider.Convert(tempFile)); } - private bool CheckDirectSupport(string filename, List supportedImageFormats) + public string ConvertFile(string filename) { - if (supportedImageFormats == null) - return false; - string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); - return supportedImageFormats.Contains(ext); - } - - public string ConvertFile(string filename, List supportedImageFormats) - { - if (CheckDirectSupport(filename, supportedImageFormats)) - return filename; IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); if (provider == null) return filename; @@ -70,13 +58,5 @@ public void ConvertDirectory(string directory) provider.Convert(filename); } } - - public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) - { - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); - if (provider == null) - return null; - return provider.GetDimensions(fileName, pageNumber); - } } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 6d9eb2b265..f8b10388e5 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -47,7 +47,7 @@ public interface IImageService /// /// /// - string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); /// /// Writes out a thumbnail by file path input /// @@ -218,7 +218,7 @@ public string GetCoverImage(string path, string fileName, string outputDirectory try { - fileName = _converterService.ConvertFile(fileName, null); + fileName = _converterService.ConvertFile(fileName); var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); @@ -246,9 +246,8 @@ public string GetCoverImage(string path, string fileName, string outputDirectory /// Where to output the file, defaults to covers directory /// Export the file as the passed encoding /// File name with extension of the file. This will always write to - public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) + public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - stream = _converterService.ConvertStream(sourceFile, stream); var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; using var sourceImage = Image.NewFromStream(stream); @@ -291,8 +290,6 @@ public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileN public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - sourceFile = _converterService.ConvertFile(sourceFile,null); - var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); From cf05e5f36eb6550c2da531f9b215ba5348054a5e Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Mon, 26 Aug 2024 00:27:21 -0300 Subject: [PATCH 06/37] Reapply "Changed the way conversion happens." This reverts commit 9d48664b08f01303f1527bc1c577e1d4376f89ff. --- API.Tests/Services/ArchiveServiceTests.cs | 6 ++-- API.Tests/Services/CacheServiceTests.cs | 16 ++++----- API/API.csproj | 3 +- API/Controllers/OPDSController.cs | 4 +-- API/Controllers/ReaderController.cs | 13 ++++--- API/Extensions/FileTypeGroupExtensions.cs | 12 ++++++- API/Extensions/HttpExtensions.cs | 32 ++++++++++++++++- API/Services/ArchiveService.cs | 15 ++++---- API/Services/BookService.cs | 6 ++-- API/Services/CacheService.cs | 36 ++++++++++++------- .../JpegXLImageConverterProvider.cs | 16 +++++++++ API/Services/ImageConverterService.cs | 24 +++++++++++-- API/Services/ImageService.cs | 9 +++-- 13 files changed, 140 insertions(+), 52 deletions(-) diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 27a93ea649..530affbf31 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -30,7 +30,7 @@ public ArchiveServiceTests(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; _archiveService = new ArchiveService(_logger, _directoryService, new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()), - Substitute.For(), Substitute.For()); + Substitute.For()); } [Theory] @@ -224,9 +224,9 @@ public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOu public void CanParseCoverImage(string inputFile) { var imageService = Substitute.For(); - imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); - var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For(),Substitute.For()); + var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile)); var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output"); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index ba06525a35..c5ae07bee4 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; @@ -158,7 +158,7 @@ public async Task Ensure_DirectoryAlreadyExists_DontExtractAnything() new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); await ResetDB(); var s = new SeriesBuilder("Test").Build(); @@ -234,7 +234,7 @@ public void CleanupChapters_AllFilesShouldBeDeleted() var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); @@ -256,7 +256,7 @@ public void GetCachedEpubFile_ShouldReturnFirstEpub() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); var c = new ChapterBuilder("1") .WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build()) @@ -297,7 +297,7 @@ public void GetCachedPagePath_ReturnNullIfNoFiles() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(),Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -341,7 +341,7 @@ public void GetCachedPagePath_GetFileFromFirstFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -382,7 +382,7 @@ public void GetCachedPagePath_GetLastPageFromSingleFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -427,7 +427,7 @@ public void GetCachedPagePath_GetFileFromSecondFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); diff --git a/API/API.csproj b/API/API.csproj index 9cdecc8d1b..1755146ff9 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -13,8 +13,7 @@ - - + diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index d25321ad8a..0b260a5379 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1228,7 +1228,7 @@ public async Task GetPageStreamedImage(string apiKey, [FromQuery] try { - var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); + var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber, Request.SupportedImageTypesFromRequest()); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); @@ -1252,7 +1252,7 @@ await _readerService.SaveReadingProgress(new ProgressDto() }, userId); } - return File(content, MimeTypeMap.GetMimeType(format)); + return File(content, format.GetMimeType()); } catch (Exception) { diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 159c8c922b..99d096f749 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -23,6 +23,7 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using MimeTypes; +using Org.BouncyCastle.Ocsp; namespace API.Controllers; @@ -118,12 +119,12 @@ public async Task GetImage(int chapterId, int page, string apiKey, var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); _logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId); - var path = _cacheService.GetCachedPagePath(chapter.Id, page); + var path = _cacheService.GetCachedPagePath(chapter.Id, page, Request.SupportedImageTypesFromRequest()); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); - return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true); + return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path), true); } catch (Exception) { @@ -132,6 +133,8 @@ public async Task GetImage(int chapterId, int page, string apiKey, } } + + /// /// Returns a thumbnail for the given page number /// @@ -180,11 +183,11 @@ public async Task GetBookmarkImage(int seriesId, string apiKey, in try { - var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); + var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page, Request.SupportedImageTypesFromRequest()); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); var format = Path.GetExtension(path); - return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path)); + return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path)); } catch (Exception) { @@ -729,7 +732,7 @@ public async Task BookmarkPage(BookmarkDto bookmarkDto) if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find")); bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); - var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); + var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page, Request.SupportedImageTypesFromRequest()); if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save")); diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index 24073f6424..9cbd330980 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -1,6 +1,7 @@ -using System; +using System; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; +using MimeTypes; namespace API.Extensions; @@ -22,4 +23,13 @@ public static string GetRegex(this FileTypeGroup fileTypeGroup) throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); } } + public static string GetMimeType(this string format) + { + //Add jxl format + format = format.ToLowerInvariant(); + if (format == ".jxl") + return "image/jxl"; + return MimeTypeMap.GetMimeType(format); + } + } diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index fbf8281044..6287f31f89 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,5 +1,8 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -53,4 +56,31 @@ public static void AddCacheHeader(this HttpResponse response, string filename, i response.Headers.CacheControl = $"max-age={maxAge}"; } } + + public static List SupportedImageTypesFromRequest(this HttpRequest request) + { + var acceptHeader = request.Headers["Accept"]; + string[] spl1 = acceptHeader.ToString().Split(';'); + acceptHeader = spl1[0]; + string[] split = acceptHeader.ToString().Split(','); + List defaultExtensions = new List(); + defaultExtensions.Add("jpeg"); + defaultExtensions.Add("jpg"); + defaultExtensions.Add("png"); + defaultExtensions.Add("gif"); + + foreach (string v in split) + { + if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) + { + string n = v.Substring(6); + if (n == "svg+xml") + n = "svg"; + if (n.StartsWith("*")) + continue; + defaultExtensions.Add(n.ToLowerInvariant()); + } + } + return defaultExtensions; + } } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 8a42844020..641c25c0d3 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -58,17 +58,16 @@ public class ArchiveService : IArchiveService private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly IMediaErrorService _mediaErrorService; - private readonly IImageConverterService _converterService; + private const string ComicInfoFilename = "ComicInfo.xml"; public ArchiveService(ILogger logger, IDirectoryService directoryService, - IImageService imageService, IMediaErrorService mediaErrorService, IImageConverterService converterService) + IImageService imageService, IMediaErrorService mediaErrorService) { _logger = logger; _directoryService = directoryService; _imageService = imageService; _mediaErrorService = mediaErrorService; - _converterService = converterService; } /// @@ -233,8 +232,8 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, archive.Entries.Select(e => e.FullName)); var entry = archive.Entries.Single(e => e.FullName == entryName); - using var stream = _converterService.ConvertStream(entryName, entry.Open()); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + using var stream = entry.Open(); + return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: { @@ -244,8 +243,8 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entryName = FindCoverImageFilename(archivePath, entryNames); var entry = archive.Entries.Single(e => e.Key == entryName); - using var stream = _converterService.ConvertStream(entryName, entry.OpenEntryStream()); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + using var stream = entry.OpenEntryStream(); + return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); @@ -562,7 +561,6 @@ public void ExtractArchive(string archivePath, string extractPath) { using var archive = ZipFile.OpenRead(archivePath); ExtractArchiveEntries(archive, extractPath); - _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.SharpCompress: @@ -571,7 +569,6 @@ public void ExtractArchive(string archivePath, string extractPath) ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)), extractPath); - _converterService.ConvertDirectory(extractPath); break; } case ArchiveLibrary.NotSupported: diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index e4ed920473..cb22f24d66 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -1229,7 +1229,7 @@ public string GetCoverImage(string fileFilePath, string fileName, string outputD if (coverImageContent == null) return string.Empty; using var stream = coverImageContent.GetContentStream(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { @@ -1252,7 +1252,7 @@ private string GetPdfCoverImage(string fileFilePath, string fileName, string out using var stream = StreamManager.GetStream("BookService.GetPdfPage"); GetPdfPage(docReader, 0, stream); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index c6e5393486..a00df5879f 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -13,6 +13,7 @@ using API.Extensions; using Kavita.Common; using Microsoft.Extensions.Logging; +using MimeKit.Tnef; using NetVips; namespace API.Services; @@ -34,12 +35,12 @@ public interface ICacheService /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. void CleanupChapters(IEnumerable chapterIds); void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(int chapterId, int page); + string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null); string GetCachePath(int chapterId); string GetBookmarkCachePath(int seriesId); IEnumerable GetCachedPages(int chapterId); IEnumerable GetCachedFileDimensions(string cachePath); - string GetCachedBookmarkPagePath(int seriesId, int page); + string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null); string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); Task CacheBookmarkForSeries(int userId, int seriesId); @@ -52,18 +53,19 @@ public class CacheService : ICacheService private readonly IDirectoryService _directoryService; private readonly IReadingItemService _readingItemService; private readonly IBookmarkService _bookmarkService; - + private readonly IImageConverterService _converterService; private static readonly ConcurrentDictionary ExtractLocks = new(); public CacheService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IReadingItemService readingItemService, - IBookmarkService bookmarkService) + IBookmarkService bookmarkService, IImageConverterService converterService) { _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; _readingItemService = readingItemService; _bookmarkService = bookmarkService; + _converterService = converterService; } public IEnumerable GetCachedPages(int chapterId) @@ -98,13 +100,19 @@ public IEnumerable GetCachedFileDimensions(string cachePath) for (var i = 0; i < files.Length; i++) { var file = files[i]; - using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); + var dimension = _converterService.GetDimensions(file, i); + if (dimension == null) + { + using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); + dimension = (image.Width, image.Height); + } + dimensions.Add(new FileDimensionDto() { PageNumber = i, - Height = image.Height, - Width = image.Width, - IsWide = image.Width > image.Height, + Height = dimension.Value.Height, + Width = dimension.Value.Width, + IsWide = dimension.Value.Width > dimension.Value.Height, FileName = file.Replace(cachePath, string.Empty) }); } @@ -122,7 +130,7 @@ public IEnumerable GetCachedFileDimensions(string cachePath) return dimensions; } - public string GetCachedBookmarkPagePath(int seriesId, int page) + public string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null) { // Calculate what chapter the page belongs to var path = GetBookmarkCachePath(seriesId); @@ -138,7 +146,8 @@ public string GetCachedBookmarkPagePath(int seriesId, int page) } // Since array is 0 based, we need to keep that in account (only affects last image) - return page == files.Length ? files[page - 1] : files[page]; + string file=page == files.Length ? files[page - 1] : files[page]; + return _converterService.ConvertFile(file, supportedImageFormats); } /// @@ -316,7 +325,7 @@ public string GetBookmarkCachePath(int seriesId) /// Chapter id with Files populated. /// Page number to look for /// Page filepath or empty if no files found. - public string GetCachedPagePath(int chapterId, int page) + public string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null) { // Calculate what chapter the page belongs to var path = GetCachePath(chapterId); @@ -325,7 +334,8 @@ public string GetCachedPagePath(int chapterId, int page) //.OrderByNatural(Path.GetFileNameWithoutExtension) // This is already done in GetPageFromFiles .ToArray(); - return GetPageFromFiles(files, page); + string file = GetPageFromFiles(files, page); + return _converterService.ConvertFile(file, supportedImageFormats); } public async Task CacheBookmarkForSeries(int userId, int seriesId) diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 95b399afd2..3bd1a2f113 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using System.Threading; using Kavita.Common.EnvironmentInfo; @@ -10,6 +11,7 @@ public interface IImageConverterProvider string Extension { get; } bool IsSupported(string filename); string Convert(string filename); + (int Width, int Height)? GetDimensions(string fileName, int pageNumber); } public class JpegXLImageConverterProvider : IImageConverterProvider @@ -37,6 +39,8 @@ internal bool AppFound } private string exeFile => OsInfo.IsWindows ? "djxl.exe" : "djxl"; + private string infoFile => OsInfo.IsWindows ? "jxlinfo.exe" : "jxlinfo"; + public bool IsSupported(string filename) { if (AppFound) @@ -44,6 +48,18 @@ public bool IsSupported(string filename) return false; } + private static Regex dimensions = new Regex(@",\s?(\d+)x(\d+),", RegexOptions.Compiled); + public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) + { + if (!AppFound) + return null; + string output = OsInfo.RunAndCapture(infoFile, "\"" + fileName + "\"", Timeout.Infinite); + Match m= dimensions.Match(output); + if (!m.Success) + return null; + return (int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value)); + } + public string Extension => ".jxl"; public string Convert(string filename) { diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index 08cab27a87..e2f61dd7a6 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -6,14 +6,16 @@ using System.Linq; using API.Services.ImageConversion; using static Org.BouncyCastle.Bcpg.Attr.ImageAttrib; +using API.DTOs.Reader; namespace API.Services { public interface IImageConverterService { Stream ConvertStream(string filename, Stream source); - string ConvertFile(string filename); + string ConvertFile(string filename, List supportedImageFormats); void ConvertDirectory(string directory); + (int Width, int Height)? GetDimensions(string fileName, int pageNumber); } public class ImageConverterService : IImageConverterService @@ -41,8 +43,18 @@ public Stream ConvertStream(string filename, Stream source) return File.OpenRead(provider.Convert(tempFile)); } - public string ConvertFile(string filename) + private bool CheckDirectSupport(string filename, List supportedImageFormats) { + if (supportedImageFormats == null) + return false; + string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); + return supportedImageFormats.Contains(ext); + } + + public string ConvertFile(string filename, List supportedImageFormats) + { + if (CheckDirectSupport(filename, supportedImageFormats)) + return filename; IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); if (provider == null) return filename; @@ -58,5 +70,13 @@ public void ConvertDirectory(string directory) provider.Convert(filename); } } + + public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) + { + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); + if (provider == null) + return null; + return provider.GetDimensions(fileName, pageNumber); + } } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index f8b10388e5..6d9eb2b265 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -47,7 +47,7 @@ public interface IImageService /// /// /// - string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); /// /// Writes out a thumbnail by file path input /// @@ -218,7 +218,7 @@ public string GetCoverImage(string path, string fileName, string outputDirectory try { - fileName = _converterService.ConvertFile(fileName); + fileName = _converterService.ConvertFile(fileName, null); var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); @@ -246,8 +246,9 @@ public string GetCoverImage(string path, string fileName, string outputDirectory /// Where to output the file, defaults to covers directory /// Export the file as the passed encoding /// File name with extension of the file. This will always write to - public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) + public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { + stream = _converterService.ConvertStream(sourceFile, stream); var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; using var sourceImage = Image.NewFromStream(stream); @@ -290,6 +291,8 @@ public string WriteCoverThumbnail(Stream stream, string fileName, string outputD public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { + sourceFile = _converterService.ConvertFile(sourceFile,null); + var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); From 4902a74dddd25f591e37b6794f7fc358e6895523 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 23 Aug 2024 18:34:58 -0300 Subject: [PATCH 07/37] Changed the way conversion happens. 1. Supported Image Formats came in the accept flag from the browser. 2. If jxl (or future formats, ie browser not supported AVIF, HEIF, etc) are not supported, Kavita will convert it to a suitable display format via conversion providers. 3. if it supported it will return the image as is, no conversion needed. 4. Thumbnails are always converted. --- API.Tests/API.Tests.csproj | 2 +- API/API.csproj | 22 +++--- API/Controllers/OPDSController.cs | 7 +- API/Controllers/ReaderController.cs | 13 +++- .../ApplicationServiceExtensions.cs | 3 + API/Extensions/HttpExtensions.cs | 15 +++- API/Services/CacheService.cs | 50 ++++++------ .../AvifImageConverterProvider.cs | 44 +++++++++++ .../HeifImageConverterProvider.cs | 27 +++++++ .../Jpeg2000ImageConverterProvider.cs | 25 ++++++ .../JpegXLImageConverterProvider.cs | 76 ++++++++----------- API/Services/ImageConverterService.cs | 35 ++++----- API/Services/ImageService.cs | 11 +-- API/Services/Plus/ExternalMetadataService.cs | 6 +- API/Services/Plus/RecommendationService.cs | 5 +- API/Services/Plus/ScrobblingService.cs | 5 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 2 +- API/Services/Tasks/StatsService.cs | 5 +- API/Services/Tasks/VersionUpdaterService.cs | 8 +- Dockerfile | 2 +- Kavita.Common/Helpers/FlurlConfiguration.cs | 30 ++++++++ .../Helpers/UntrustedCertClientFactory.cs | 13 ---- Kavita.Common/Kavita.Common.csproj | 2 +- ...uild-jxl.sh => docker-build-wconvertion.sh | 0 24 files changed, 258 insertions(+), 150 deletions(-) create mode 100644 API/Services/ImageConversion/AvifImageConverterProvider.cs create mode 100644 API/Services/ImageConversion/HeifImageConverterProvider.cs create mode 100644 API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs create mode 100644 Kavita.Common/Helpers/FlurlConfiguration.cs delete mode 100644 Kavita.Common/Helpers/UntrustedCertClientFactory.cs rename docker-build-jxl.sh => docker-build-wconvertion.sh (100%) diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index edf1af5eb6..7f2a97e252 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/API/API.csproj b/API/API.csproj index 1755146ff9..d052172237 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -12,10 +12,11 @@ latestmajor - - - - + + + + + false ../favicon.ico @@ -53,23 +54,24 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + - + @@ -99,9 +101,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 0b260a5379..8082691a24 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -43,6 +43,7 @@ public class OpdsController : BaseApiController private readonly ISeriesService _seriesService; private readonly IAccountService _accountService; private readonly ILocalizationService _localizationService; + private readonly IImageConverterService _converterService; private readonly IMapper _mapper; @@ -81,6 +82,7 @@ public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService, ICacheService cacheService, IReaderService readerService, ISeriesService seriesService, IAccountService accountService, ILocalizationService localizationService, + IImageConverterService converterService, IMapper mapper) { _unitOfWork = unitOfWork; @@ -91,6 +93,7 @@ public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, _seriesService = seriesService; _accountService = accountService; _localizationService = localizationService; + _converterService = converterService; _mapper = mapper; _xmlSerializer = new XmlSerializer(typeof(Feed)); @@ -1228,10 +1231,10 @@ public async Task GetPageStreamedImage(string apiKey, [FromQuery] try { - var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); - + path = _converterService.ConvertFile(path, Request.SupportedImageTypesFromRequest()); var content = await _directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 99d096f749..fe5303036b 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -43,6 +43,7 @@ public class ReaderController : BaseApiController private readonly IEventHub _eventHub; private readonly IScrobblingService _scrobblingService; private readonly ILocalizationService _localizationService; + private readonly IImageConverterService _converterService; /// public ReaderController(ICacheService cacheService, @@ -50,7 +51,8 @@ public ReaderController(ICacheService cacheService, IReaderService readerService, IBookmarkService bookmarkService, IAccountService accountService, IEventHub eventHub, IScrobblingService scrobblingService, - ILocalizationService localizationService) + ILocalizationService localizationService, + IImageConverterService converterService) { _cacheService = cacheService; _unitOfWork = unitOfWork; @@ -61,6 +63,7 @@ public ReaderController(ICacheService cacheService, _eventHub = eventHub; _scrobblingService = scrobblingService; _localizationService = localizationService; + _converterService = converterService; } /// @@ -119,9 +122,10 @@ public async Task GetImage(int chapterId, int page, string apiKey, var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); _logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId); - var path = _cacheService.GetCachedPagePath(chapter.Id, page, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); + path = _converterService.ConvertFile(path, Request.SupportedImageTypesFromRequest()); var format = Path.GetExtension(path); return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path), true); @@ -183,8 +187,9 @@ public async Task GetBookmarkImage(int seriesId, string apiKey, in try { - var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); + path = _converterService.ConvertFile(path, Request.SupportedImageTypesFromRequest()); var format = Path.GetExtension(path); return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path)); @@ -732,7 +737,7 @@ public async Task BookmarkPage(BookmarkDto bookmarkDto) if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find")); bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); - var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page, Request.SupportedImageTypesFromRequest()); + var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save")); diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 011acffc51..33085263e0 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -36,6 +36,9 @@ public static void AddApplicationServices(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 6287f31f89..67ffbc14b1 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -68,17 +68,26 @@ public static List SupportedImageTypesFromRequest(this HttpRequest reque defaultExtensions.Add("jpg"); defaultExtensions.Add("png"); defaultExtensions.Add("gif"); - + defaultExtensions.Add("webp"); foreach (string v in split) { if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) { - string n = v.Substring(6); + string n = v.Substring(6).ToLowerInvariant(); if (n == "svg+xml") n = "svg"; + if (n=="jp2") + defaultExtensions.Add("j2k"); + if (n == "j2k") + defaultExtensions.Add("jp2"); + if (n=="heif") + defaultExtensions.Add("heic"); + if (n == "heic") + defaultExtensions.Add("heif"); if (n.StartsWith("*")) continue; - defaultExtensions.Add(n.ToLowerInvariant()); + if (!defaultExtensions.Contains(n)) + defaultExtensions.Add(n); } } return defaultExtensions; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index a00df5879f..d5f9cc9711 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -13,8 +13,9 @@ using API.Extensions; using Kavita.Common; using Microsoft.Extensions.Logging; -using MimeKit.Tnef; using NetVips; +using static System.Net.Mime.MediaTypeNames; +using Image = NetVips.Image; namespace API.Services; #nullable enable @@ -35,12 +36,12 @@ public interface ICacheService /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. void CleanupChapters(IEnumerable chapterIds); void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null); + string GetCachedPagePath(int chapterId, int page); string GetCachePath(int chapterId); string GetBookmarkCachePath(int seriesId); IEnumerable GetCachedPages(int chapterId); IEnumerable GetCachedFileDimensions(string cachePath); - string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null); + string GetCachedBookmarkPagePath(int seriesId, int page); string GetCachedFile(Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); Task CacheBookmarkForSeries(int userId, int seriesId); @@ -54,6 +55,7 @@ public class CacheService : ICacheService private readonly IReadingItemService _readingItemService; private readonly IBookmarkService _bookmarkService; private readonly IImageConverterService _converterService; + private static readonly ConcurrentDictionary ExtractLocks = new(); public CacheService(ILogger logger, IUnitOfWork unitOfWork, @@ -100,13 +102,13 @@ public IEnumerable GetCachedFileDimensions(string cachePath) for (var i = 0; i < files.Length; i++) { var file = files[i]; - var dimension = _converterService.GetDimensions(file, i); + var dimension = _converterService.GetDimensions(file); if (dimension == null) { using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); dimension = (image.Width, image.Height); } - + dimensions.Add(new FileDimensionDto() { PageNumber = i, @@ -130,7 +132,7 @@ public IEnumerable GetCachedFileDimensions(string cachePath) return dimensions; } - public string GetCachedBookmarkPagePath(int seriesId, int page, List supportedImageFormats = null) + public string GetCachedBookmarkPagePath(int seriesId, int page) { // Calculate what chapter the page belongs to var path = GetBookmarkCachePath(seriesId); @@ -146,8 +148,7 @@ public string GetCachedBookmarkPagePath(int seriesId, int page, List sup } // Since array is 0 based, we need to keep that in account (only affects last image) - string file=page == files.Length ? files[page - 1] : files[page]; - return _converterService.ConvertFile(file, supportedImageFormats); + return page == files.Length ? files[page - 1] : files[page]; } /// @@ -247,23 +248,23 @@ public void ExtractChapterFiles(string extractPath, IReadOnlyList? fi break; case MangaFormat.Epub: case MangaFormat.Pdf: - { - if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) - { - _logger.LogError("{File} does not exist on disk", files[0].FilePath); - throw new KavitaException($"{files[0].FilePath} does not exist on disk"); - } - if (extractPdfImages) { - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) + { + _logger.LogError("{File} does not exist on disk", files[0].FilePath); + throw new KavitaException($"{files[0].FilePath} does not exist on disk"); + } + if (extractPdfImages) + { + _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + break; + } + removeNonImages = false; + + _directoryService.ExistOrCreate(extractPath); + _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); break; } - removeNonImages = false; - - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); - break; - } } } @@ -325,7 +326,7 @@ public string GetBookmarkCachePath(int seriesId) /// Chapter id with Files populated. /// Page number to look for /// Page filepath or empty if no files found. - public string GetCachedPagePath(int chapterId, int page, List supportedImageFormats = null) + public string GetCachedPagePath(int chapterId, int page) { // Calculate what chapter the page belongs to var path = GetCachePath(chapterId); @@ -334,8 +335,7 @@ public string GetCachedPagePath(int chapterId, int page, List supportedI //.OrderByNatural(Path.GetFileNameWithoutExtension) // This is already done in GetPageFromFiles .ToArray(); - string file = GetPageFromFiles(files, page); - return _converterService.ConvertFile(file, supportedImageFormats); + return GetPageFromFiles(files, page); } public async Task CacheBookmarkForSeries(int userId, int seriesId) diff --git a/API/Services/ImageConversion/AvifImageConverterProvider.cs b/API/Services/ImageConversion/AvifImageConverterProvider.cs new file mode 100644 index 0000000000..c90120f0f9 --- /dev/null +++ b/API/Services/ImageConversion/AvifImageConverterProvider.cs @@ -0,0 +1,44 @@ +using Kavita.Common.EnvironmentInfo; +using Microsoft.Extensions.Logging; +using System.Threading; +using System; +using System.IO; +using NetVips; + +namespace API.Services.ImageConversion +{ + //AVIF & HEIF are supported by netvips. + + public class AvifImageConverterProvider : IImageConverterProvider + { + private readonly ILogger _logger; + public bool IsVipsSupported => true; + + public AvifImageConverterProvider(ILogger logger) + { + _logger = logger; + } + + public bool IsSupported(string filename) + { + return filename.EndsWith(".avif", StringComparison.InvariantCultureIgnoreCase); + + } + + public string Convert(string filename) + { + string destination = Path.ChangeExtension(filename, "jpg"); + using var sourceImage = Image.NewFromFile(filename, false, Enums.Access.SequentialUnbuffered); + sourceImage.WriteToFile(destination + "[Q=99]"); + File.Delete(filename); + return destination; + } + + public (int Width, int Height)? GetDimensions(string fileName) + { + using var image = Image.NewFromFile(fileName, memory: false, access: Enums.Access.SequentialUnbuffered); + return (image.Width, image.Height); + } + + } +} diff --git a/API/Services/ImageConversion/HeifImageConverterProvider.cs b/API/Services/ImageConversion/HeifImageConverterProvider.cs new file mode 100644 index 0000000000..a7d97606e5 --- /dev/null +++ b/API/Services/ImageConversion/HeifImageConverterProvider.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using NetVips; +using System.IO; +using System; +using ImageMagick; + +namespace API.Services.ImageConversion +{ + public class HeifImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider + { + private readonly ILogger _logger; + public bool IsVipsSupported => false; + + public HeifImageConverterProvider(ILogger logger) + { + _logger = logger; + } + + public bool IsSupported(string filename) + { + return filename.EndsWith(".heif", StringComparison.InvariantCultureIgnoreCase) + || filename.EndsWith(".heic", StringComparison.InvariantCultureIgnoreCase); + + } + + } +} diff --git a/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs b/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs new file mode 100644 index 0000000000..16131d2a30 --- /dev/null +++ b/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs @@ -0,0 +1,25 @@ +using Kavita.Common.EnvironmentInfo; +using Microsoft.Extensions.Logging; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using System; + +namespace API.Services.ImageConversion +{ + //NetVips do not support Jpeg2000, Vips does but generate the right bindings is a pain in the ass + public class Jpeg2000ImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider + { + private readonly ILogger _logger; + public Jpeg2000ImageConverterProvider(ILogger logger) + { + _logger = logger; + } + + public bool IsVipsSupported => false; + public bool IsSupported(string filename) + { + return filename.EndsWith(".jp2", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".j2k", StringComparison.InvariantCultureIgnoreCase); + } + } +} diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 3bd1a2f113..2b4c41a5ce 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -1,73 +1,57 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Text.RegularExpressions; using System.Threading; +using ImageMagick; using Kavita.Common.EnvironmentInfo; +using Microsoft.Extensions.Logging; +using NetVips; namespace API.Services.ImageConversion; public interface IImageConverterProvider { - string Extension { get; } bool IsSupported(string filename); string Convert(string filename); - (int Width, int Height)? GetDimensions(string fileName, int pageNumber); + (int Width, int Height)? GetDimensions(string fileName); + + bool IsVipsSupported { get; } } -public class JpegXLImageConverterProvider : IImageConverterProvider +public class ImageMagickConverterProvider { - private bool? _appFound = null; - - internal bool AppFound + public virtual string Convert(string filename) { - get - { - if (_appFound == null) - { - try - { - _appFound = OsInfo.RunAndCapture(exeFile, "--version", Timeout.Infinite).Contains("JPEG XL"); - } - catch (Exception e) - { - //Eat it - _appFound = false; - } - } - return _appFound.Value; - } + string destination = Path.ChangeExtension(filename, "jpg"); + using var sourceImage = new MagickImage(filename); + sourceImage.Quality = 99; + sourceImage.Write(destination); + File.Delete(filename); + return destination; } - private string exeFile => OsInfo.IsWindows ? "djxl.exe" : "djxl"; - - private string infoFile => OsInfo.IsWindows ? "jxlinfo.exe" : "jxlinfo"; - public bool IsSupported(string filename) + public virtual (int Width, int Height)? GetDimensions(string filename) { - if (AppFound) - return filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); - return false; + var info = new MagickImageInfo(filename); + return (info.Width, info.Height); } +} - private static Regex dimensions = new Regex(@",\s?(\d+)x(\d+),", RegexOptions.Compiled); - public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) +//NetVips do not support Jpeg-XL, Vips does but generate the right bindings is a pain in the ass +public class JpegXLImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider +{ + private readonly ILogger _logger; + public JpegXLImageConverterProvider(ILogger logger) { - if (!AppFound) - return null; - string output = OsInfo.RunAndCapture(infoFile, "\"" + fileName + "\"", Timeout.Infinite); - Match m= dimensions.Match(output); - if (!m.Success) - return null; - return (int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value)); + _logger = logger; } + public bool IsVipsSupported => false; - public string Extension => ".jxl"; - public string Convert(string filename) + public bool IsSupported(string filename) { - if (!AppFound) - return filename; - string destination = Path.ChangeExtension(filename, "jpg"); - OsInfo.RunAndCapture(exeFile, "\"" + filename + "\" \"" + destination + "\"", Timeout.Infinite); - File.Delete(filename); - return destination; + return filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); } + + } diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index e2f61dd7a6..35a21b510e 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -1,12 +1,9 @@ -using NetVips; using System.Collections; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using API.Services.ImageConversion; -using static Org.BouncyCastle.Bcpg.Attr.ImageAttrib; -using API.DTOs.Reader; namespace API.Services { @@ -14,17 +11,16 @@ public interface IImageConverterService { Stream ConvertStream(string filename, Stream source); string ConvertFile(string filename, List supportedImageFormats); - void ConvertDirectory(string directory); - (int Width, int Height)? GetDimensions(string fileName, int pageNumber); + (int Width, int Height)? GetDimensions(string fileName); + + public bool IsVipsSupported(string filename); } public class ImageConverterService : IImageConverterService { private IEnumerable _converters; - private readonly IDirectoryService _directoryService; - public ImageConverterService(IDirectoryService directoryService, IEnumerable converters) + public ImageConverterService(IEnumerable converters) { - _directoryService = directoryService; _converters = converters; } @@ -35,7 +31,7 @@ public Stream ConvertStream(string filename, Stream source) IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); if (provider == null) return source; - string tempFile = Path.ChangeExtension(Path.GetFileName(filename), provider.Extension); + string tempFile = Path.GetFileName(filename); Stream dest = File.OpenWrite(tempFile); source.CopyTo(dest); source.Close(); @@ -61,22 +57,21 @@ public string ConvertFile(string filename, List supportedImageFormats) return provider.Convert(filename); } - public void ConvertDirectory(string directory) - { - foreach (string filename in Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories)) - { - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); - if (provider != null) - provider.Convert(filename); - } - } - public (int Width, int Height)? GetDimensions(string fileName, int pageNumber) + public (int Width, int Height)? GetDimensions(string fileName) { IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); if (provider == null) return null; - return provider.GetDimensions(fileName, pageNumber); + return provider.GetDimensions(fileName); + } + + public bool IsVipsSupported(string filename) + { + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); + if (provider == null) + return true; + return provider.IsVipsSupported; } } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 6d9eb2b265..bd45060688 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -137,7 +137,6 @@ public void ExtractImages(string? fileFilePath, string targetDirectory, int file _directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, Tasks.Scanner.Parser.Parser.ImageFileExtensions); } - _converterService.ConvertDirectory(targetDirectory); } /// @@ -218,7 +217,8 @@ public string GetCoverImage(string path, string fileName, string outputDirectory try { - fileName = _converterService.ConvertFile(fileName, null); + if (!_converterService.IsVipsSupported(fileName)) + fileName = _converterService.ConvertFile(fileName, null); var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); @@ -248,7 +248,8 @@ public string GetCoverImage(string path, string fileName, string outputDirectory /// File name with extension of the file. This will always write to public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - stream = _converterService.ConvertStream(sourceFile, stream); + if (!_converterService.IsVipsSupported(sourceFile)) + stream = _converterService.ConvertStream(sourceFile, stream); var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; using var sourceImage = Image.NewFromStream(stream); @@ -291,8 +292,8 @@ public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileN public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - sourceFile = _converterService.ConvertFile(sourceFile,null); - + if (!_converterService.IsVipsSupported(sourceFile)) + sourceFile = _converterService.ConvertFile(sourceFile, null); var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 4bc55eb88d..a453239ca9 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -91,9 +91,7 @@ public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } /// diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs index 24cb1445b0..da96f1fef0 100644 --- a/API/Services/Plus/RecommendationService.cs +++ b/API/Services/Plus/RecommendationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -37,8 +37,7 @@ public RecommendationService(IUnitOfWork unitOfWork, ILogger - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } public async Task GetRecommendationsForSeries(int userId, int seriesId) diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index 51939198bd..c24a3d3143 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -107,8 +107,7 @@ public ScrobblingService(IUnitOfWork unitOfWork, IEventHub eventHub, ILogger - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 78f784cdd6..a22c1b903e 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -25,7 +25,7 @@ public static class Parser public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); - public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif|\.jxl)"; // Don't forget to update CoverChooser + public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif|\.jxl|\.heif|\.heic|\.j2k|\.jp2)"; // Don't forget to update CoverChooser public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; public const string EpubFileExtension = @"\.epub"; public const string PdfFileExtension = @"\.pdf"; diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 9cc93fefd4..f9bfff2b64 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -46,8 +46,7 @@ public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataCo _context = context; _statisticService = statisticService; - FlurlHttp.ConfigureClient(ApiUrl, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlConfiguration.ConfigureClientForUrl(ApiUrl); } /// diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index f1a6eb383a..3f1d40f7f1 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -64,10 +64,8 @@ public VersionUpdaterService(ILogger logger, IEventHub ev _logger = logger; _eventHub = eventHub; - FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); - FlurlHttp.ConfigureClient(GithubAllReleasesUrl, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlConfiguration.ConfigureClientForUrl(GithubLatestReleasesUrl); + FlurlConfiguration.ConfigureClientForUrl(GithubAllReleasesUrl); } /// diff --git a/Dockerfile b/Dockerfile index 3eb3417c57..3e531ff0b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ COPY API/config/appsettings.json /tmp/config/appsettings.json #Installs program dependencies RUN apt-get update \ - && apt-get install -y libicu-dev libssl3t64 libgdiplus curl libjxl-tools \ + && apt-get install -y libicu-dev libssl3t64 libgdiplus curl libjxl0.10 libavif-bin libheif1 libopenjp2-7 \ && rm -rf /var/lib/apt/lists/* COPY entrypoint.sh /entrypoint.sh diff --git a/Kavita.Common/Helpers/FlurlConfiguration.cs b/Kavita.Common/Helpers/FlurlConfiguration.cs new file mode 100644 index 0000000000..7d43d8ec53 --- /dev/null +++ b/Kavita.Common/Helpers/FlurlConfiguration.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Flurl.Http; + + +namespace Kavita.Common.Helpers; + +public static class FlurlConfiguration +{ + private static readonly List _configuredClients = new List(); + private static readonly object _lock = new object(); + + public static void ConfigureClientForUrl(string url) + { + lock (_lock) + { + Uri ur = new Uri(url); + string host = ur.Host+":"+ur.Port; + if (_configuredClients.Contains(host)) + { + return; + } + FlurlHttp.ConfigureClientForUrl(url).ConfigureInnerHandler(cli => + cli.ServerCertificateCustomValidationCallback = (_, _, _, _) => true); + _configuredClients.Add(host); + } + } + +} diff --git a/Kavita.Common/Helpers/UntrustedCertClientFactory.cs b/Kavita.Common/Helpers/UntrustedCertClientFactory.cs deleted file mode 100644 index 6ddb2a9f33..0000000000 --- a/Kavita.Common/Helpers/UntrustedCertClientFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net.Http; -using Flurl.Http.Configuration; - -namespace Kavita.Common.Helpers; - -public class UntrustedCertClientFactory : DefaultHttpClientFactory -{ - public override HttpMessageHandler CreateMessageHandler() { - return new HttpClientHandler { - ServerCertificateCustomValidationCallback = (_, _, _, _) => true - }; - } -} diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index c4d23536d0..a4e8401974 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -11,7 +11,7 @@ - + diff --git a/docker-build-jxl.sh b/docker-build-wconvertion.sh similarity index 100% rename from docker-build-jxl.sh rename to docker-build-wconvertion.sh From 7d48f6ec1002592dd8e8103397466a02a3c9a993 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Mon, 26 Aug 2024 21:55:16 -0300 Subject: [PATCH 08/37] Add Documentation --- API/Extensions/FileTypeGroupExtensions.cs | 14 ++++- API/Extensions/HttpExtensions.cs | 11 ++-- .../AvifImageConverterProvider.cs | 30 +++++++++-- .../HeifImageConverterProvider.cs | 18 ++++++- .../Jpeg2000ImageConverterProvider.cs | 18 ++++++- .../JpegXLImageConverterProvider.cs | 53 +++++++++++++++++-- API/Services/ImageConverterService.cs | 44 +++++++++++++-- Kavita.Common/Helpers/FlurlConfiguration.cs | 14 +++-- 8 files changed, 179 insertions(+), 23 deletions(-) diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index 9cbd330980..ca6c1c187f 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -7,6 +7,11 @@ namespace API.Extensions; public static class FileTypeGroupExtensions { + /// + /// Gets the regular expression pattern for the specified FileTypeGroup. + /// + /// The FileTypeGroup. + /// The regular expression pattern. public static string GetRegex(this FileTypeGroup fileTypeGroup) { switch (fileTypeGroup) @@ -23,13 +28,18 @@ public static string GetRegex(this FileTypeGroup fileTypeGroup) throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); } } + + /// + /// Gets the MIME type for the specified file format. Extends original MimeTypeMap adding non supported extensions by the nuget. + /// + /// The file format. + /// The MIME type. public static string GetMimeType(this string format) { - //Add jxl format + // Add jxl format format = format.ToLowerInvariant(); if (format == ".jxl") return "image/jxl"; return MimeTypeMap.GetMimeType(format); } - } diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 67ffbc14b1..3c7ac456c1 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -57,6 +57,11 @@ public static void AddCacheHeader(this HttpResponse response, string filename, i } } + /// + /// Retrieves the supported image types extensions from the Accept header of the HTTP request. + /// + /// The HTTP request. + /// A list of supported image types extensions. public static List SupportedImageTypesFromRequest(this HttpRequest request) { var acceptHeader = request.Headers["Accept"]; @@ -76,18 +81,18 @@ public static List SupportedImageTypesFromRequest(this HttpRequest reque string n = v.Substring(6).ToLowerInvariant(); if (n == "svg+xml") n = "svg"; - if (n=="jp2") + if (n == "jp2") defaultExtensions.Add("j2k"); if (n == "j2k") defaultExtensions.Add("jp2"); - if (n=="heif") + if (n == "heif") defaultExtensions.Add("heic"); if (n == "heic") defaultExtensions.Add("heif"); if (n.StartsWith("*")) continue; if (!defaultExtensions.Contains(n)) - defaultExtensions.Add(n); + defaultExtensions.Add(n); } } return defaultExtensions; diff --git a/API/Services/ImageConversion/AvifImageConverterProvider.cs b/API/Services/ImageConversion/AvifImageConverterProvider.cs index c90120f0f9..59b060177f 100644 --- a/API/Services/ImageConversion/AvifImageConverterProvider.cs +++ b/API/Services/ImageConversion/AvifImageConverterProvider.cs @@ -7,24 +7,42 @@ namespace API.Services.ImageConversion { - //AVIF & HEIF are supported by netvips. - + /// + /// Provides image conversion functionality for AVIF format using netvips library. + /// public class AvifImageConverterProvider : IImageConverterProvider { private readonly ILogger _logger; + + /// + /// Gets a value indicating whether Vips supports AVIF. + /// public bool IsVipsSupported => true; + /// + /// Initializes a new instance of the class. + /// + /// The logger. public AvifImageConverterProvider(ILogger logger) { _logger = logger; } + /// + /// Checks if the filename has the AVIF extension. + /// + /// The filename of the image file. + /// True if the image is AVIF image type; otherwise, false. public bool IsSupported(string filename) { return filename.EndsWith(".avif", StringComparison.InvariantCultureIgnoreCase); - } + /// + /// Converts the specified AVIF file to JPEG format. + /// + /// The filename of the AVIF file. + /// The filename of the converted JPEG file. public string Convert(string filename) { string destination = Path.ChangeExtension(filename, "jpg"); @@ -34,11 +52,15 @@ public string Convert(string filename) return destination; } + /// + /// Gets the dimensions (width and height) of the specified image file. + /// + /// The filename of the image file. + /// The dimensions of the image file as a tuple of width and height. public (int Width, int Height)? GetDimensions(string fileName) { using var image = Image.NewFromFile(fileName, memory: false, access: Enums.Access.SequentialUnbuffered); return (image.Width, image.Height); } - } } diff --git a/API/Services/ImageConversion/HeifImageConverterProvider.cs b/API/Services/ImageConversion/HeifImageConverterProvider.cs index a7d97606e5..2e5565a945 100644 --- a/API/Services/ImageConversion/HeifImageConverterProvider.cs +++ b/API/Services/ImageConversion/HeifImageConverterProvider.cs @@ -6,22 +6,36 @@ namespace API.Services.ImageConversion { + /// + /// Provides image conversion functionality for HEIF and HEIC file formats using ImageMagick. + /// public class HeifImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider { private readonly ILogger _logger; + + /// + /// Gets a value indicating whether Vips supports HEIF. + /// public bool IsVipsSupported => false; + /// + /// Initializes a new instance of the class. + /// + /// The logger. public HeifImageConverterProvider(ILogger logger) { _logger = logger; } + /// + /// Checks if the filename has the HEIF/HEIC extension. + /// + /// The filename of the image file. + /// True if the image is HEIF/HEIC image type; otherwise, false. public bool IsSupported(string filename) { return filename.EndsWith(".heif", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".heic", StringComparison.InvariantCultureIgnoreCase); - } - } } diff --git a/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs b/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs index 16131d2a30..6c9e8f5ddd 100644 --- a/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs +++ b/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs @@ -7,16 +7,32 @@ namespace API.Services.ImageConversion { - //NetVips do not support Jpeg2000, Vips does but generate the right bindings is a pain in the ass + /// + /// Provides image conversion functionality for JPEG2000 images. + /// public class Jpeg2000ImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider { private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. public Jpeg2000ImageConverterProvider(ILogger logger) { _logger = logger; } + /// + /// Gets a value indicating whether Vips supports JPEG 2000. + /// public bool IsVipsSupported => false; + + /// + /// Checks if the filename has the JPEG 2000 extension. + /// + /// The filename of the image file. + /// True if the image is JPEG 2000 image type; otherwise, false. public bool IsSupported(string filename) { return filename.EndsWith(".jp2", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".j2k", StringComparison.InvariantCultureIgnoreCase); diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 2b4c41a5ce..3e23d46d43 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -10,17 +10,45 @@ namespace API.Services.ImageConversion; +/// +/// Represents an image converter provider. +/// public interface IImageConverterProvider { + /// + /// Checks if the image converter supports the image format. + /// + /// The filename of the image file. + /// True if the image converter supports the image type; otherwise, false. bool IsSupported(string filename); + + /// + /// Converts the specified image to JPG. + /// + /// The filename of the image file. + /// The converted image file. string Convert(string filename); + + /// + /// Gets the dimensions of the specified image file. + /// + /// The filename of the image file. + /// The dimensions of the image file as a tuple of width and height, or null if the dimensions cannot be determined. (int Width, int Height)? GetDimensions(string fileName); + /// + /// Gets a value indicating whether the image type supports Vips. + /// bool IsVipsSupported { get; } } public class ImageMagickConverterProvider { + /// + /// Converts the specified image to JPG. + /// + /// The filename of the image file. + /// The converted image file. public virtual string Convert(string filename) { string destination = Path.ChangeExtension(filename, "jpg"); @@ -31,6 +59,11 @@ public virtual string Convert(string filename) return destination; } + /// + /// Gets the dimensions of the specified image file. + /// + /// The filename of the image file. + /// The dimensions of the image file as a tuple of width and height, or null if the dimensions cannot be determined. public virtual (int Width, int Height)? GetDimensions(string filename) { var info = new MagickImageInfo(filename); @@ -38,20 +71,34 @@ public virtual (int Width, int Height)? GetDimensions(string filename) } } -//NetVips do not support Jpeg-XL, Vips does but generate the right bindings is a pain in the ass +/// +/// Represents a JPEG-XL image converter provider. +/// public class JpegXLImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider { private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. public JpegXLImageConverterProvider(ILogger logger) { _logger = logger; } + + /// + /// Gets a value indicating whether Vips supports JPEG-XL. + /// public bool IsVipsSupported => false; + /// + /// Checks if the filename has the JPEG-XL extension. + /// + /// The filename of the image file. + /// True if the image is JPEG-XL image type; otherwise, false. public bool IsSupported(string filename) { return filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); } - - } diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index 35a21b510e..dc95460582 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -7,25 +7,59 @@ namespace API.Services { + /// + /// Represents an image converter service. + /// public interface IImageConverterService { + /// + /// Converts the input stream to jpg image format. + /// + /// The filename of the image. + /// The input stream of the image. + /// The converted image stream. Stream ConvertStream(string filename, Stream source); + + /// + /// Converts the specified image file to jpg image format. + /// + /// The filename of the image. + /// The list of supported image formats by the client from the Accept Header. + /// The converted image file path if conversion is required. string ConvertFile(string filename, List supportedImageFormats); + + /// + /// Gets the dimensions (width and height) of the specified image file. + /// + /// The filename of the image. + /// The dimensions of the image (width and height). (int Width, int Height)? GetDimensions(string fileName); - public bool IsVipsSupported(string filename); + /// + /// Checks if the VIPS library supports for the specified image type. + /// + /// The filename of the image. + /// True if the VIPS library support it; otherwise, false. + bool IsVipsSupported(string filename); } + /// + /// Represents an implementation of the image converter service. + /// public class ImageConverterService : IImageConverterService { private IEnumerable _converters; + + /// + /// Initializes a new instance of the class. + /// + /// The collection of image converter providers. public ImageConverterService(IEnumerable converters) { _converters = converters; } - - + /// public Stream ConvertStream(string filename, Stream source) { IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); @@ -47,6 +81,7 @@ private bool CheckDirectSupport(string filename, List supportedImageForm return supportedImageFormats.Contains(ext); } + /// public string ConvertFile(string filename, List supportedImageFormats) { if (CheckDirectSupport(filename, supportedImageFormats)) @@ -57,7 +92,7 @@ public string ConvertFile(string filename, List supportedImageFormats) return provider.Convert(filename); } - + /// public (int Width, int Height)? GetDimensions(string fileName) { IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); @@ -66,6 +101,7 @@ public string ConvertFile(string filename, List supportedImageFormats) return provider.GetDimensions(fileName); } + /// public bool IsVipsSupported(string filename) { IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); diff --git a/Kavita.Common/Helpers/FlurlConfiguration.cs b/Kavita.Common/Helpers/FlurlConfiguration.cs index 7d43d8ec53..44aed280b7 100644 --- a/Kavita.Common/Helpers/FlurlConfiguration.cs +++ b/Kavita.Common/Helpers/FlurlConfiguration.cs @@ -6,25 +6,31 @@ namespace Kavita.Common.Helpers; +/// +/// Helper class for configuring Flurl client for a specific URL. +/// public static class FlurlConfiguration { private static readonly List _configuredClients = new List(); private static readonly object _lock = new object(); + /// + /// Configures the Flurl client for the specified URL. + /// + /// The URL to configure the client for. public static void ConfigureClientForUrl(string url) { + //Important client are mapped without path, per example two urls pointing to the same host:port but different path, will use the same client. lock (_lock) { Uri ur = new Uri(url); - string host = ur.Host+":"+ur.Port; + //key is host:port + string host = ur.Host + ":" + ur.Port; if (_configuredClients.Contains(host)) - { return; - } FlurlHttp.ConfigureClientForUrl(url).ConfigureInnerHandler(cli => cli.ServerCertificateCustomValidationCallback = (_, _, _, _) => true); _configuredClients.Add(host); } } - } From a4a12a1c58eebc65a723c58f15cb12e721cd20b5 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Wed, 28 Aug 2024 18:17:56 -0300 Subject: [PATCH 09/37] FIX BOMS --- API.Benchmark/ArchiveServiceBenchmark.cs | 2 +- API.Tests/Services/ArchiveServiceTests.cs | 2 +- API.Tests/Services/BookServiceTests.cs | 2 +- API.Tests/Services/CacheServiceTests.cs | 2 +- API/Extensions/ApplicationServiceExtensions.cs | 2 +- API/Extensions/FileTypeGroupExtensions.cs | 2 +- API/Extensions/HttpExtensions.cs | 2 +- API/Services/ArchiveService.cs | 2 +- API/Services/BookService.cs | 2 +- API/Services/CacheService.cs | 2 +- API/Services/ImageService.cs | 2 +- API/Services/Plus/ExternalMetadataService.cs | 2 +- API/Services/Plus/RecommendationService.cs | 2 +- API/Services/Plus/ScrobblingService.cs | 2 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 2 +- API/Services/Tasks/StatsService.cs | 2 +- API/Services/Tasks/VersionUpdaterService.cs | 2 +- Kavita.Common/EnvironmentInfo/IOsInfo.cs | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index fd82ee87d4..10dc367a4c 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using Microsoft.Extensions.Logging.Abstractions; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 530affbf31..982fa26062 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index 2fc1e18d12..b24085bded 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.IO.Abstractions; using API.Services; using EasyCaching.Core; diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index c5ae07bee4..45a86d04f3 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 33085263e0..b6154710b1 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,4 +1,4 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using API.Constants; using API.Data; using API.Helpers; diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index ca6c1c187f..87eac43bb9 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; using MimeTypes; diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 3c7ac456c1..8cd1c7b771 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 641c25c0d3..5ce662bedc 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index cb22f24d66..5e339b2101 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index d5f9cc9711..c8e48d4c3d 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 7176198682..0960b2a7ac 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index a453239ca9..e9759f26fc 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs index da96f1fef0..5b38a943be 100644 --- a/API/Services/Plus/RecommendationService.cs +++ b/API/Services/Plus/RecommendationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index c24a3d3143..2efd9ea269 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index a22c1b903e..ef6fe2e9b4 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Immutable; using System.Globalization; using System.IO; diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index f9bfff2b64..8c1d461792 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 3f1d40f7f1..4a42a34ebf 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs index 6b99e0eac1..a015e60ecb 100644 --- a/Kavita.Common/EnvironmentInfo/IOsInfo.cs +++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; namespace Kavita.Common.EnvironmentInfo; From 0f98d7507a30d96e462c1d4e3c236ded336ef6c5 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Wed, 28 Aug 2024 18:29:51 -0300 Subject: [PATCH 10/37] Recode SupportedImageTypesFromRequest removing possible errors. --- API/Extensions/HttpExtensions.cs | 49 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 8cd1c7b771..2394db2c0c 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -57,6 +57,14 @@ public static void AddCacheHeader(this HttpResponse response, string filename, i } } + private static void AddExtension(List extensions, string extension) + { + if (string.IsNullOrEmpty(extension)) + return; + if (!extensions.Contains(extension)) + extensions.Add(extension); + } + /// /// Retrieves the supported image types extensions from the Accept header of the HTTP request. /// @@ -64,37 +72,40 @@ public static void AddCacheHeader(this HttpResponse response, string filename, i /// A list of supported image types extensions. public static List SupportedImageTypesFromRequest(this HttpRequest request) { - var acceptHeader = request.Headers["Accept"]; - string[] spl1 = acceptHeader.ToString().Split(';'); + var acceptHeader = request.Headers["Accept"].ToString(); + string[] spl1 = acceptHeader.Split(';'); acceptHeader = spl1[0]; - string[] split = acceptHeader.ToString().Split(','); - List defaultExtensions = new List(); - defaultExtensions.Add("jpeg"); - defaultExtensions.Add("jpg"); - defaultExtensions.Add("png"); - defaultExtensions.Add("gif"); - defaultExtensions.Add("webp"); + string[] split = acceptHeader.Split(','); + List supportedExtensions = new List(); + + //Add default extensions supported by all browsers. + supportedExtensions.Add("jpeg"); + supportedExtensions.Add("jpg"); + supportedExtensions.Add("png"); + supportedExtensions.Add("gif"); + supportedExtensions.Add("webp"); + //Browser add specific image mime types, when the image type is not a global standard. + //Let's reuse that to identify the additional image types supported by the browser. foreach (string v in split) { if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) { string n = v.Substring(6).ToLowerInvariant(); + if (n.StartsWith("*")) + continue; if (n == "svg+xml") n = "svg"; if (n == "jp2") - defaultExtensions.Add("j2k"); + AddExtension(supportedExtensions, "j2k"); if (n == "j2k") - defaultExtensions.Add("jp2"); + AddExtension(supportedExtensions, "jp2"); if (n == "heif") - defaultExtensions.Add("heic"); + AddExtension(supportedExtensions, "heic"); if (n == "heic") - defaultExtensions.Add("heif"); - if (n.StartsWith("*")) - continue; - if (!defaultExtensions.Contains(n)) - defaultExtensions.Add(n); + AddExtension(supportedExtensions, "heif"); + AddExtension(supportedExtensions, n); } } - return defaultExtensions; + return supportedExtensions; } } From 937f89cbfdad1a8eefa5a8a6fbc35830ef20cf97 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Wed, 28 Aug 2024 18:46:34 -0300 Subject: [PATCH 11/37] Remove own docker build --- docker-build-wconvertion.sh | 103 ------------------------------------ 1 file changed, 103 deletions(-) delete mode 100644 docker-build-wconvertion.sh diff --git a/docker-build-wconvertion.sh b/docker-build-wconvertion.sh deleted file mode 100644 index 206b97f0b6..0000000000 --- a/docker-build-wconvertion.sh +++ /dev/null @@ -1,103 +0,0 @@ -#! /bin/bash -set -e - -outputFolder='_output' - -ProgressStart() -{ - echo "Start '$1'" -} - -ProgressEnd() -{ - echo "Finish '$1'" -} - -Build() -{ - local RID="$1" - - ProgressStart "Build for $RID" - - slnFile=Kavita.sln - - dotnet clean $slnFile -c Release - - dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID - - ProgressEnd "Build for $RID" -} - -BuildUI() -{ - ProgressStart 'Building UI' - echo 'Removing old wwwroot' - rm -rf API/wwwroot/* - cd UI/Web/ || exit - echo 'Installing web dependencies' - npm install --legacy-peer-deps - echo 'Building UI' - npm run prod - echo 'Copying back to Kavita wwwroot' - mkdir -p ../../API/wwwroot - cp -R dist/browser/* ../../API/wwwroot - cd ../../ || exit - ProgressEnd 'Building UI' -} - -Package() -{ - local runtime="$1" - local lOutputFolder=../_output/"$runtime"/Kavita - - ProgressStart "Creating $runtime Package" - - # TODO: Use no-restore? Because Build should have already done it for us - echo "Building" - cd API - echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" - dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" - - echo "Copying Install information" - cp ../INSTALL.txt "$lOutputFolder"/README.txt - - echo "Copying LICENSE" - cp ../LICENSE "$lOutputFolder"/LICENSE.txt - - echo "Renaming API -> Kavita" - mv "$lOutputFolder"/API "$lOutputFolder"/Kavita - - echo "Creating tar" - cd ../$outputFolder/"$runtime"/ - tar -czvf ../kavita-$runtime.tar.gz Kavita - - ProgressEnd "Creating $runtime Package" - -} - -dir=$PWD - -if [ -d _output ] -then - rm -r _output/ -fi - -BuildUI - -#Build for x64 -Build "linux-x64" -Package "linux-x64" -cd "$dir" - -#Build for arm -Build "linux-arm" -Package "linux-arm" -cd "$dir" - -#Build for arm64 -Build "linux-arm64" -Package "linux-arm64" -cd "$dir" - -#Builds Docker images -docker buildx build -t maxpiva/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push From a08a7957aa092494668b1277195a7be8d0a84400 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Wed, 28 Aug 2024 20:06:30 -0300 Subject: [PATCH 12/37] Shell Call no longer used, since swapped image conversion to Image Magick. Rollback IOsInfo.cs --- Kavita.Common/EnvironmentInfo/IOsInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs index a015e60ecb..d8cc6a070e 100644 --- a/Kavita.Common/EnvironmentInfo/IOsInfo.cs +++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs @@ -57,7 +57,7 @@ private static Os GetPosixFlavour() } } - public static string RunAndCapture(string filename, string args, int waitInMilliseconds = 1000) + private static string RunAndCapture(string filename, string args) { var p = new Process { @@ -75,7 +75,7 @@ public static string RunAndCapture(string filename, string args, int waitInMilli // To avoid deadlocks, always read the output stream first and then wait. var output = p.StandardOutput.ReadToEnd(); - p.WaitForExit(waitInMilliseconds); + p.WaitForExit(1000); return output; } From e7f021a5634be185be2fca0d5372b853d234112d Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 29 Aug 2024 19:22:04 -0300 Subject: [PATCH 13/37] Edit CodeStyle Fix GetCoverImage, trying to create a temporary image in current directory, and rename the parameter field name, that generates a wrong asumption. --- API/Controllers/ReaderController.cs | 1 - API/Extensions/FileTypeGroupExtensions.cs | 7 +- API/Extensions/HttpExtensions.cs | 46 +++++++--- API/Services/CacheService.cs | 7 +- API/Services/ImageConverterService.cs | 28 +++--- API/Services/ImageService.cs | 12 +-- docker-build-wconvertion.sh | 103 ++++++++++++++++++++++ 7 files changed, 166 insertions(+), 38 deletions(-) create mode 100644 docker-build-wconvertion.sh diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index d951c3eaac..973085c026 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -23,7 +23,6 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using MimeTypes; -using Org.BouncyCastle.Ocsp; namespace API.Controllers; diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index 87eac43bb9..254ff400d9 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; using MimeTypes; @@ -38,8 +38,9 @@ public static string GetMimeType(this string format) { // Add jxl format format = format.ToLowerInvariant(); - if (format == ".jxl") - return "image/jxl"; + + if (format == ".jxl") return "image/jxl"; + return MimeTypeMap.GetMimeType(format); } } diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 2394db2c0c..d7bef84797 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -73,12 +73,16 @@ private static void AddExtension(List extensions, string extension) public static List SupportedImageTypesFromRequest(this HttpRequest request) { var acceptHeader = request.Headers["Accept"].ToString(); - string[] spl1 = acceptHeader.Split(';'); - acceptHeader = spl1[0]; - string[] split = acceptHeader.Split(','); + var split = acceptHeader.Split(';'); + acceptHeader = split[0]; + split = acceptHeader.Split(','); + List supportedExtensions = new List(); //Add default extensions supported by all browsers. + + //NOTE: Will user parser, instead of this list, but first fixing low-hanging fruits, and showstopper issues. + supportedExtensions.Add("jpeg"); supportedExtensions.Add("jpg"); supportedExtensions.Add("png"); @@ -90,22 +94,38 @@ public static List SupportedImageTypesFromRequest(this HttpRequest reque { if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) { - string n = v.Substring(6).ToLowerInvariant(); - if (n.StartsWith("*")) - continue; - if (n == "svg+xml") - n = "svg"; - if (n == "jp2") + string mimeimagepart = v.Substring(6).ToLowerInvariant(); + if (mimeimagepart.StartsWith("*")) continue; + + if (mimeimagepart == "svg+xml") + { + mimeimagepart = "svg"; + } + + if (mimeimagepart == "jp2") + { AddExtension(supportedExtensions, "j2k"); - if (n == "j2k") + } + + if (mimeimagepart == "j2k") + { AddExtension(supportedExtensions, "jp2"); - if (n == "heif") + } + + if (mimeimagepart == "heif") + { AddExtension(supportedExtensions, "heic"); - if (n == "heic") + } + + if (mimeimagepart == "heic") + { AddExtension(supportedExtensions, "heif"); - AddExtension(supportedExtensions, n); + } + + AddExtension(supportedExtensions, mimeimagepart); } } + return supportedExtensions; } } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index c8e48d4c3d..040acf38ae 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -103,11 +103,6 @@ public IEnumerable GetCachedFileDimensions(string cachePath) { var file = files[i]; var dimension = _converterService.GetDimensions(file); - if (dimension == null) - { - using var image = Image.NewFromFile(file, memory: false, access: Enums.Access.SequentialUnbuffered); - dimension = (image.Width, image.Height); - } dimensions.Add(new FileDimensionDto() { diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index dc95460582..050b1cc633 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using System.Linq; using API.Services.ImageConversion; +using NetVips; namespace API.Services { @@ -15,10 +16,11 @@ public interface IImageConverterService /// /// Converts the input stream to jpg image format. /// - /// The filename of the image. + /// The OriginalNameWithExtension of the image + /// (Not a file, only the name, if it came from the filesystem or an archive entry). /// The input stream of the image. /// The converted image stream. - Stream ConvertStream(string filename, Stream source); + Stream ConvertStream(string originalNameWithExtension, Stream source); /// /// Converts the specified image file to jpg image format. @@ -60,17 +62,19 @@ public ImageConverterService(IEnumerable converters) } /// - public Stream ConvertStream(string filename, Stream source) + public Stream ConvertStream(string originalNameWithExtension, Stream source) { - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); + //Check if the name have one of our supported extensions. + IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(originalNameWithExtension)); if (provider == null) return source; - string tempFile = Path.GetFileName(filename); - Stream dest = File.OpenWrite(tempFile); + //We support it, create a temp file, and write the original content, so it can be transcoded into another stream. + string tempFileName = Path.ChangeExtension(Path.GetTempFileName(), Path.GetExtension(originalNameWithExtension)); + Stream dest = File.OpenWrite(tempFileName); source.CopyTo(dest); source.Close(); dest.Close(); - return File.OpenRead(provider.Convert(tempFile)); + return File.OpenRead(provider.Convert(tempFileName)); } private bool CheckDirectSupport(string filename, List supportedImageFormats) @@ -95,10 +99,14 @@ public string ConvertFile(string filename, List supportedImageFormats) /// public (int Width, int Height)? GetDimensions(string fileName) { + //Provider supported IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); - if (provider == null) - return null; - return provider.GetDimensions(fileName); + if (provider != null) + return provider.GetDimensions(fileName); + + //No provider for this image type, so, is a common image format, use netvips and original code. + using var image = Image.NewFromFile(fileName, memory: false, access: Enums.Access.SequentialUnbuffered); + return (image.Width, image.Height); } /// diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 0960b2a7ac..775af8d383 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; @@ -47,7 +47,7 @@ public interface IImageService /// /// /// - string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + string WriteCoverThumbnail(string originalNameWithExtension, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); /// /// Writes out a thumbnail by file path input /// @@ -241,15 +241,17 @@ public string GetCoverImage(string path, string fileName, string outputDirectory /// Creates a thumbnail out of a memory stream and saves to with the passed /// fileName and the appropriate extension. /// + /// The OriginalNameWithExtension of the image + /// (Not a file, only the name, if it came from the filesystem or an archive entry). /// 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 the passed encoding /// File name with extension of the file. This will always write to - public string WriteCoverThumbnail(string sourceFile, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) + public string WriteCoverThumbnail(string originalNameWithExtension, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - if (!_converterService.IsVipsSupported(sourceFile)) - stream = _converterService.ConvertStream(sourceFile, stream); + if (!_converterService.IsVipsSupported(originalNameWithExtension)) + stream = _converterService.ConvertStream(originalNameWithExtension, stream); var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; using var sourceImage = Image.NewFromStream(stream); diff --git a/docker-build-wconvertion.sh b/docker-build-wconvertion.sh new file mode 100644 index 0000000000..206b97f0b6 --- /dev/null +++ b/docker-build-wconvertion.sh @@ -0,0 +1,103 @@ +#! /bin/bash +set -e + +outputFolder='_output' + +ProgressStart() +{ + echo "Start '$1'" +} + +ProgressEnd() +{ + echo "Finish '$1'" +} + +Build() +{ + local RID="$1" + + ProgressStart "Build for $RID" + + slnFile=Kavita.sln + + dotnet clean $slnFile -c Release + + dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID + + ProgressEnd "Build for $RID" +} + +BuildUI() +{ + ProgressStart 'Building UI' + echo 'Removing old wwwroot' + rm -rf API/wwwroot/* + cd UI/Web/ || exit + echo 'Installing web dependencies' + npm install --legacy-peer-deps + echo 'Building UI' + npm run prod + echo 'Copying back to Kavita wwwroot' + mkdir -p ../../API/wwwroot + cp -R dist/browser/* ../../API/wwwroot + cd ../../ || exit + ProgressEnd 'Building UI' +} + +Package() +{ + local runtime="$1" + local lOutputFolder=../_output/"$runtime"/Kavita + + ProgressStart "Creating $runtime Package" + + # TODO: Use no-restore? Because Build should have already done it for us + echo "Building" + cd API + echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" + dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" + + echo "Copying Install information" + cp ../INSTALL.txt "$lOutputFolder"/README.txt + + echo "Copying LICENSE" + cp ../LICENSE "$lOutputFolder"/LICENSE.txt + + echo "Renaming API -> Kavita" + mv "$lOutputFolder"/API "$lOutputFolder"/Kavita + + echo "Creating tar" + cd ../$outputFolder/"$runtime"/ + tar -czvf ../kavita-$runtime.tar.gz Kavita + + ProgressEnd "Creating $runtime Package" + +} + +dir=$PWD + +if [ -d _output ] +then + rm -r _output/ +fi + +BuildUI + +#Build for x64 +Build "linux-x64" +Package "linux-x64" +cd "$dir" + +#Build for arm +Build "linux-arm" +Package "linux-arm" +cd "$dir" + +#Build for arm64 +Build "linux-arm64" +Package "linux-arm64" +cd "$dir" + +#Builds Docker images +docker buildx build -t maxpiva/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push From c624287b444770a55c242ee59b30a44781803443 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 29 Aug 2024 19:26:13 -0300 Subject: [PATCH 14/37] Remove custom docker builder --- docker-build-wconvertion.sh | 103 ------------------------------------ 1 file changed, 103 deletions(-) delete mode 100644 docker-build-wconvertion.sh diff --git a/docker-build-wconvertion.sh b/docker-build-wconvertion.sh deleted file mode 100644 index 206b97f0b6..0000000000 --- a/docker-build-wconvertion.sh +++ /dev/null @@ -1,103 +0,0 @@ -#! /bin/bash -set -e - -outputFolder='_output' - -ProgressStart() -{ - echo "Start '$1'" -} - -ProgressEnd() -{ - echo "Finish '$1'" -} - -Build() -{ - local RID="$1" - - ProgressStart "Build for $RID" - - slnFile=Kavita.sln - - dotnet clean $slnFile -c Release - - dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform="Any CPU" -p:RuntimeIdentifiers=$RID - - ProgressEnd "Build for $RID" -} - -BuildUI() -{ - ProgressStart 'Building UI' - echo 'Removing old wwwroot' - rm -rf API/wwwroot/* - cd UI/Web/ || exit - echo 'Installing web dependencies' - npm install --legacy-peer-deps - echo 'Building UI' - npm run prod - echo 'Copying back to Kavita wwwroot' - mkdir -p ../../API/wwwroot - cp -R dist/browser/* ../../API/wwwroot - cd ../../ || exit - ProgressEnd 'Building UI' -} - -Package() -{ - local runtime="$1" - local lOutputFolder=../_output/"$runtime"/Kavita - - ProgressStart "Creating $runtime Package" - - # TODO: Use no-restore? Because Build should have already done it for us - echo "Building" - cd API - echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" - dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" - - echo "Copying Install information" - cp ../INSTALL.txt "$lOutputFolder"/README.txt - - echo "Copying LICENSE" - cp ../LICENSE "$lOutputFolder"/LICENSE.txt - - echo "Renaming API -> Kavita" - mv "$lOutputFolder"/API "$lOutputFolder"/Kavita - - echo "Creating tar" - cd ../$outputFolder/"$runtime"/ - tar -czvf ../kavita-$runtime.tar.gz Kavita - - ProgressEnd "Creating $runtime Package" - -} - -dir=$PWD - -if [ -d _output ] -then - rm -r _output/ -fi - -BuildUI - -#Build for x64 -Build "linux-x64" -Package "linux-x64" -cd "$dir" - -#Build for arm -Build "linux-arm" -Package "linux-arm" -cd "$dir" - -#Build for arm64 -Build "linux-arm64" -Package "linux-arm64" -cd "$dir" - -#Builds Docker images -docker buildx build -t maxpiva/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push From 6dbeaf1f76d928f59282de2c39044b682288d518 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 30 Aug 2024 17:59:52 -0300 Subject: [PATCH 15/37] Fixed discovered issues. Optimize Cover Creation --- .../AvifImageConverterProvider.cs | 10 ++++ .../JpegXLImageConverterProvider.cs | 33 ++++++++++++ API/Services/ImageConverterService.cs | 52 +++++++++++-------- API/Services/ImageService.cs | 15 +++--- 4 files changed, 80 insertions(+), 30 deletions(-) diff --git a/API/Services/ImageConversion/AvifImageConverterProvider.cs b/API/Services/ImageConversion/AvifImageConverterProvider.cs index 59b060177f..607f7ca1e1 100644 --- a/API/Services/ImageConversion/AvifImageConverterProvider.cs +++ b/API/Services/ImageConversion/AvifImageConverterProvider.cs @@ -4,6 +4,7 @@ using System; using System.IO; using NetVips; +using System.IO.Abstractions; namespace API.Services.ImageConversion { @@ -51,6 +52,15 @@ public string Convert(string filename) File.Delete(filename); return destination; } + /// + /// Creates a NetVips Image object from the specified image stream. + /// + /// The source image stream. + /// The NetVips Image object created from the image stream. + public virtual Image ImageFromStream(Stream source) + { + return Image.NewFromStream(source); + } /// /// Gets the dimensions (width and height) of the specified image file. diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs index 3e23d46d43..60d73ce172 100644 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.IO.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using ImageMagick; @@ -40,6 +42,13 @@ public interface IImageConverterProvider /// Gets a value indicating whether the image type supports Vips. /// bool IsVipsSupported { get; } + + /// + /// Creates a NetVips Image object from the specified image stream. + /// + /// The source image stream. + /// The NetVips Image object created from the image stream. + Image ImageFromStream(Stream source); } public class ImageMagickConverterProvider @@ -59,6 +68,30 @@ public virtual string Convert(string filename) return destination; } + /// + /// Creates a NetVips Image object from the specified image stream. + /// + /// The source image stream. + /// The NetVips Image object created from the image stream. + public virtual Image ImageFromStream(Stream source) + { + var settings = new MagickReadSettings + { + ColorSpace = ColorSpace.sRGB + }; + using var sourceImage = new MagickImage(source, settings); + float[] pixels = sourceImage.GetPixels().ToArray(); + float mul = 1F / 255F; + for (int x = 0; x < pixels.Length; x++) + pixels[x] *= mul; + GCHandle handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); + IntPtr pointer = handle.AddrOfPinnedObject(); + ulong size = (ulong)(Marshal.SizeOf() * pixels.Length); + Image im = Image.NewFromMemoryCopy(pointer, size, sourceImage.Width, sourceImage.Height,3, Enums.BandFormat.Float); + handle.Free(); + return im; + } + /// /// Gets the dimensions of the specified image file. /// diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index 050b1cc633..4fa98d4e02 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -1,9 +1,11 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.Linq; using API.Services.ImageConversion; +using Microsoft.Extensions.Logging; using NetVips; namespace API.Services @@ -14,24 +16,24 @@ namespace API.Services public interface IImageConverterService { /// - /// Converts the input stream to jpg image format. + /// Converts the input stream into a Vips Image. /// /// The OriginalNameWithExtension of the image - /// (Not a file, only the name, if it came from the filesystem or an archive entry). + /// (Not a file, only the name, if it came from the filesystem or an archive entry to identify the image type). /// The input stream of the image. - /// The converted image stream. - Stream ConvertStream(string originalNameWithExtension, Stream source); + /// NetVips Image from the input Stream. + Image GetImageFromStream(string originalNameWithExtension, Stream source); /// /// Converts the specified image file to jpg image format. /// /// The filename of the image. - /// The list of supported image formats by the client from the Accept Header. - /// The converted image file path if conversion is required. - string ConvertFile(string filename, List supportedImageFormats); + /// The list of supported image formats by the client from the Accept Header. If null is passed, the conversion will execute if the image requires conversion. + /// The converted image file path if conversion is required. Note: original filename is deleted if conversion is executed + string ConvertFile(string filename, List supportedImageFormats = null); /// - /// Gets the dimensions (width and height) of the specified image file. + /// Gets the dimensions (width and height) of the specified image file without read the whole image. /// /// The filename of the image. /// The dimensions of the image (width and height). @@ -50,31 +52,32 @@ public interface IImageConverterService /// public class ImageConverterService : IImageConverterService { - private IEnumerable _converters; + private readonly IEnumerable _converters; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// + /// The logger instance. /// The collection of image converter providers. - public ImageConverterService(IEnumerable converters) + public ImageConverterService(ILogger logger, IEnumerable converters) { + _logger = logger; _converters = converters; } /// - public Stream ConvertStream(string originalNameWithExtension, Stream source) + public Image GetImageFromStream(string originalNameWithExtension, Stream source) { //Check if the name have one of our supported extensions. IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(originalNameWithExtension)); - if (provider == null) - return source; - //We support it, create a temp file, and write the original content, so it can be transcoded into another stream. - string tempFileName = Path.ChangeExtension(Path.GetTempFileName(), Path.GetExtension(originalNameWithExtension)); - Stream dest = File.OpenWrite(tempFileName); - source.CopyTo(dest); - source.Close(); - dest.Close(); - return File.OpenRead(provider.Convert(tempFileName)); + + if (provider == null || provider.IsVipsSupported) return Image.NewFromStream(source); //No need to transform, Vips supports the format. + + var sw = Stopwatch.StartNew(); + Image image = provider.ImageFromStream(source); + _logger.LogDebug("Image converted from '{Extension}' to '.jpg' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(originalNameWithExtension), sw.ElapsedMilliseconds); + return image; } private bool CheckDirectSupport(string filename, List supportedImageFormats) @@ -86,14 +89,17 @@ private bool CheckDirectSupport(string filename, List supportedImageForm } /// - public string ConvertFile(string filename, List supportedImageFormats) + public string ConvertFile(string filename, List supportedImageFormats = null) { if (CheckDirectSupport(filename, supportedImageFormats)) return filename; IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); - if (provider == null) + if (provider == null) //No provider for this image type, so, is a common image format. return filename; - return provider.Convert(filename); + var sw = Stopwatch.StartNew(); + filename = provider.Convert(filename); + _logger.LogDebug("Image converted from '{Extension}' to '.jpg' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(filename), sw.ElapsedMilliseconds); + return filename; } /// diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 775af8d383..56811c785d 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -217,8 +217,9 @@ public string GetCoverImage(string path, string fileName, string outputDirectory try { - if (!_converterService.IsVipsSupported(fileName)) - fileName = _converterService.ConvertFile(fileName, null); + if (!_converterService.IsVipsSupported(path)) //If Vips supports the format, there is no need to convert the image, since in this case the consumer is not the browser. + path = _converterService.ConvertFile(path); + var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); @@ -250,11 +251,10 @@ public string GetCoverImage(string path, string fileName, string outputDirectory /// File name with extension of the file. This will always write to public string WriteCoverThumbnail(string originalNameWithExtension, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - if (!_converterService.IsVipsSupported(originalNameWithExtension)) - stream = _converterService.ConvertStream(originalNameWithExtension, stream); var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; - using var sourceImage = Image.NewFromStream(stream); + + using var sourceImage = _converterService.GetImageFromStream(originalNameWithExtension, stream); var scalingSize = GetSizeForDimensions(sourceImage, targetWidth, targetHeight); var scalingCrop = GetCropForDimensions(sourceImage, targetWidth, targetHeight); @@ -294,8 +294,9 @@ public string WriteCoverThumbnail(string originalNameWithExtension, Stream strea public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - if (!_converterService.IsVipsSupported(sourceFile)) - sourceFile = _converterService.ConvertFile(sourceFile, null); + if (!_converterService.IsVipsSupported(sourceFile)) //If Vips supports the format, there is no need to convert the image, since in this case the consumer is not the browser. + sourceFile = _converterService.ConvertFile(sourceFile); + var (width, height) = size.GetDimensions(); using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); From 0cc4803401fc0c530cbb462de9927c6fdd65a2a4 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 27 Sep 2024 22:37:06 -0300 Subject: [PATCH 16/37] Removed NetVips and ImageSharp (Use only ImageMagick) Removing ImageConvertors (No Longer Needed) Refactored mime types and extensions constants --- API.Benchmark/ArchiveServiceBenchmark.cs | 41 +++-- API.Tests/Services/ArchiveServiceTests.cs | 12 +- API.Tests/Services/ImageServiceTests.cs | 30 ++-- API/API.csproj | 5 +- .../ApplicationServiceExtensions.cs | 8 +- API/Extensions/HttpExtensions.cs | 44 ++--- API/Services/BookService.cs | 13 +- API/Services/CacheService.cs | 14 +- .../AvifImageConverterProvider.cs | 76 -------- .../HeifImageConverterProvider.cs | 41 ----- .../Jpeg2000ImageConverterProvider.cs | 41 ----- .../JpegXLImageConverterProvider.cs | 137 --------------- API/Services/ImageConverterService.cs | 79 ++------- API/Services/ImageService.cs | 166 ++++++++---------- API/Services/Tasks/Scanner/Parser/Parser.cs | 21 ++- 15 files changed, 180 insertions(+), 548 deletions(-) delete mode 100644 API/Services/ImageConversion/AvifImageConverterProvider.cs delete mode 100644 API/Services/ImageConversion/HeifImageConverterProvider.cs delete mode 100644 API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs delete mode 100644 API/Services/ImageConversion/JpegXLImageConverterProvider.cs diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index 10dc367a4c..f21d213473 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using Microsoft.Extensions.Logging.Abstractions; @@ -6,11 +6,9 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; using EasyCaching.Core; +using ImageMagick; using NSubstitute; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Processing; + namespace API.Benchmark; @@ -24,8 +22,7 @@ public class ArchiveServiceBenchmark private readonly ArchiveService _archiveService; private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; - private readonly PngEncoder _pngEncoder = new PngEncoder(); - private readonly WebpEncoder _webPEncoder = new WebpEncoder(); + private const string SourceImage = "C:/Users/josep/Pictures/obey_by_grrsa-d6llkaa_colored_by_me.png"; @@ -67,9 +64,11 @@ public void ImageSharp_ExtractImage_PNG() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail2 = SixLabors.ImageSharp.Image.Load(stream); - thumbnail2.Mutate(x => x.Resize(320, 0)); - thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png"), _pngEncoder); + using var thumbnail2 = new MagickImage(stream); + int width = 320; + int height = (int)(thumbnail2.Height * (width / (double)thumbnail2.Width)); + thumbnail2.Thumbnail(width, height); + thumbnail2.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png")); } [Benchmark] @@ -79,9 +78,11 @@ public void ImageSharp_ExtractImage_WebP() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail2 = SixLabors.ImageSharp.Image.Load(stream); - thumbnail2.Mutate(x => x.Resize(320, 0)); - thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp"), _webPEncoder); + using var thumbnail2 = new MagickImage(stream); + int width = 320; + int height = (int)(thumbnail2.Height * (width / (double)thumbnail2.Width)); + thumbnail2.Thumbnail(width, height); + thumbnail2.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp")); } [Benchmark] @@ -91,8 +92,11 @@ public void NetVips_ExtractImage_PNG() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail = NetVips.Image.ThumbnailStream(stream, 320); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.png")); + using var thumbnail = new MagickImage(stream); + int width = 320; + int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); + thumbnail.Thumbnail(width, height); + thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.png")); } [Benchmark] @@ -102,8 +106,11 @@ public void NetVips_ExtractImage_WebP() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail = NetVips.Image.ThumbnailStream(stream, 320); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.webp")); + using var thumbnail = new MagickImage(stream); + int width = 320; + int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); + thumbnail.Thumbnail(width, height); + thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.webp")); } // Benchmark to test default GetNumberOfPages from archive diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 982fa26062..9d3d06b0aa 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; @@ -8,8 +8,8 @@ using API.Entities.Enums; using API.Services; using EasyCaching.Core; +using ImageMagick; using Microsoft.Extensions.Logging; -using NetVips; using NSubstitute; using NSubstitute.Extensions; using Xunit; @@ -171,7 +171,13 @@ public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFi var archiveService = Substitute.For(_logger, ds, imageService, Substitute.For()); var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")); - var expectedBytes = Image.Thumbnail(Path.Join(testDirectory, expectedOutputFile), 320).WriteToBuffer(".png"); + using var thumbnail = new MagickImage(Path.Join(testDirectory, expectedOutputFile)); + int width = 320; + int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); + thumbnail.Thumbnail(width, height); + using MemoryStream stream = new MemoryStream(); + thumbnail.Write(stream, MagickFormat.Png32); + var expectedBytes = stream.ToArray(); archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default); diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index ac3c3157f6..66ef037e06 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using System.IO; using System.IO.Abstractions; using System.Linq; @@ -6,11 +6,12 @@ using API.Entities.Enums; using API.Services; using EasyCaching.Core; +using ImageMagick; using Microsoft.Extensions.Logging; -using NetVips; + using NSubstitute; using Xunit; -using Image = NetVips.Image; + namespace API.Tests.Services; @@ -60,17 +61,16 @@ private void GenerateFiles(string outputExtension) { var fileName = Path.GetFileNameWithoutExtension(imagePath); var dims = CoverImageSize.Default.GetDimensions(); - using var sourceImage = Image.NewFromFile(imagePath, false, Enums.Access.SequentialUnbuffered); - - var size = ImageService.GetSizeForDimensions(sourceImage, dims.Width, dims.Height); - var crop = ImageService.GetCropForDimensions(sourceImage, dims.Width, dims.Height); + using var thumbnail = new MagickImage(imagePath); - using var thumbnail = Image.Thumbnail(imagePath, dims.Width, dims.Height, - size: size, - crop: crop); + var size = ImageService.GetSizeForDimensions(thumbnail, dims.Width, dims.Height); + var crop = ImageService.GetCropForDimensions(thumbnail, dims.Width, dims.Height); + thumbnail.Thumbnail(size); + if (crop) + thumbnail.Crop(dims.Width, dims.Height, Gravity.Center); var outputFileName = fileName + outputExtension + ".png"; - thumbnail.WriteToFile(Path.Join(_testDirectory, outputFileName)); + thumbnail.Write(Path.Join(_testDirectory, outputFileName)); } } @@ -105,7 +105,7 @@ private void GenerateHtmlFile() var outputPath = Path.Combine(_testDirectory, fileName + "_output.png"); var dims = CoverImageSize.Default.GetDimensions(); - using var sourceImage = Image.NewFromFile(imagePath, false, Enums.Access.SequentialUnbuffered); + using var sourceImage = new MagickImage(imagePath); htmlBuilder.AppendLine("
"); htmlBuilder.AppendLine($"

{fileName} ({((double) sourceImage.Width / sourceImage.Height).ToString("F2")}) - {ImageService.WillScaleWell(sourceImage, dims.Width, dims.Height)}

"); htmlBuilder.AppendLine($"\"{fileName}\""); @@ -165,9 +165,9 @@ public void TestColorScapes() private static void GenerateColorImage(string hexColor, string outputPath) { var color = ImageService.HexToRgb(hexColor); - using var colorImage = Image.Black(200, 100); - using var output = colorImage + new[] { color.R / 255.0, color.G / 255.0, color.B / 255.0 }; - output.WriteToFile(outputPath); + using var colorImage = + new MagickImage(MagickColor.FromRgb(color.R, color.G, color.B), 200, 100); + colorImage.Write(outputPath); } private void GenerateHtmlFileForColorScape() diff --git a/API/API.csproj b/API/API.csproj index 8aa619457a..169ddfab90 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -1,4 +1,4 @@ - + Default @@ -83,8 +83,6 @@ - - @@ -96,7 +94,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index b6154710b1..2f774f3e99 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,9 +1,9 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using API.Constants; using API.Data; using API.Helpers; using API.Services; -using API.Services.ImageConversion; + using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Metadata; @@ -35,10 +35,6 @@ public static void AddApplicationServices(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index d7bef84797..715fd0b1b2 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.Json; using API.Helpers; +using API.Services.Tasks.Scanner.Parser; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; @@ -69,7 +70,7 @@ private static void AddExtension(List extensions, string extension) /// Retrieves the supported image types extensions from the Accept header of the HTTP request. ///
/// The HTTP request. - /// A list of supported image types extensions. + /// A list of supported image types extensions by the Browser. public static List SupportedImageTypesFromRequest(this HttpRequest request) { var acceptHeader = request.Headers["Accept"].ToString(); @@ -80,15 +81,8 @@ public static List SupportedImageTypesFromRequest(this HttpRequest reque List supportedExtensions = new List(); //Add default extensions supported by all browsers. - - //NOTE: Will user parser, instead of this list, but first fixing low-hanging fruits, and showstopper issues. - - supportedExtensions.Add("jpeg"); - supportedExtensions.Add("jpg"); - supportedExtensions.Add("png"); - supportedExtensions.Add("gif"); - supportedExtensions.Add("webp"); - //Browser add specific image mime types, when the image type is not a global standard. + supportedExtensions.AddRange(Parser.UniversalFileImageExtensionArray); + //Browser add specific image mime types, when the image type is not a global standard, browser specify the specific image type in the accept header. //Let's reuse that to identify the additional image types supported by the browser. foreach (string v in split) { @@ -96,36 +90,18 @@ public static List SupportedImageTypesFromRequest(this HttpRequest reque { string mimeimagepart = v.Substring(6).ToLowerInvariant(); if (mimeimagepart.StartsWith("*")) continue; - - if (mimeimagepart == "svg+xml") - { - mimeimagepart = "svg"; - } - - if (mimeimagepart == "jp2") + if (Parser.NonUniversalSupportedMimeMappings.ContainsKey(mimeimagepart)) { - AddExtension(supportedExtensions, "j2k"); + Parser.NonUniversalSupportedMimeMappings[mimeimagepart].ForEach(x => AddExtension(supportedExtensions, x)); } - - if (mimeimagepart == "j2k") + else if (mimeimagepart == "svg+xml") { - AddExtension(supportedExtensions, "jp2"); + AddExtension(supportedExtensions, "svg"); } - - if (mimeimagepart == "heif") - { - AddExtension(supportedExtensions, "heic"); - } - - if (mimeimagepart == "heic") - { - AddExtension(supportedExtensions, "heif"); - } - - AddExtension(supportedExtensions, mimeimagepart); + else + AddExtension(supportedExtensions, mimeimagepart); } } - return supportedExtensions; } } diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 5e339b2101..88f3cd11b6 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -18,12 +18,11 @@ using Docnet.Core.Readers; using ExCSS; using HtmlAgilityPack; +using ImageMagick; using Kavita.Common; using Microsoft.Extensions.Logging; using Microsoft.IO; using Nager.ArticleNumber; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using VersOne.Epub; using VersOne.Epub.Options; using VersOne.Epub.Schema; @@ -1277,12 +1276,14 @@ private static void GetPdfPage(IDocReader docReader, int pageNumber, Stream stre { using var pageReader = docReader.GetPageReader(pageNumber); var rawBytes = pageReader.GetImage(new NaiveTransparencyRemover()); + var floats = rawBytes.Select(a=> (float)a*256F).ToArray(); var width = pageReader.GetPageWidth(); var height = pageReader.GetPageHeight(); - var image = Image.LoadPixelData(rawBytes, width, height); - + using MagickImage image = new MagickImage(rawBytes, width, height,MagickFormat.Bgra); + using var pixels = image.GetPixels(); + pixels.SetArea(0,0,width, height, floats); stream.Seek(0, SeekOrigin.Begin); - image.SaveAsPng(stream); + image.Write(stream, MagickFormat.Png); stream.Seek(0, SeekOrigin.Begin); } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 6e66e48b9f..82ad11b0e7 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -11,11 +11,10 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; +using ImageMagick; using Kavita.Common; using Microsoft.Extensions.Logging; -using NetVips; using static System.Net.Mime.MediaTypeNames; -using Image = NetVips.Image; namespace API.Services; #nullable enable @@ -95,21 +94,19 @@ public IEnumerable GetCachedFileDimensions(string cachePath) } var dimensions = new List(); - var originalCacheSize = Cache.MaxFiles; try { - Cache.MaxFiles = 0; for (var i = 0; i < files.Length; i++) { var file = files[i]; - var dimension = _converterService.GetDimensions(file); + MagickImageInfo info = new MagickImageInfo(file); dimensions.Add(new FileDimensionDto() { PageNumber = i, - Height = dimension.Value.Height, - Width = dimension.Value.Width, - IsWide = dimension.Value.Width > dimension.Value.Height, + Height = info.Height, + Width = info.Width, + IsWide = info.Width > info.Height, FileName = file.Replace(cachePath, string.Empty) }); } @@ -120,7 +117,6 @@ public IEnumerable GetCachedFileDimensions(string cachePath) } finally { - Cache.MaxFiles = originalCacheSize; } _logger.LogDebug("File Dimensions call for {Length} images took {Time}ms", dimensions.Count, sw.ElapsedMilliseconds); diff --git a/API/Services/ImageConversion/AvifImageConverterProvider.cs b/API/Services/ImageConversion/AvifImageConverterProvider.cs deleted file mode 100644 index 607f7ca1e1..0000000000 --- a/API/Services/ImageConversion/AvifImageConverterProvider.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Kavita.Common.EnvironmentInfo; -using Microsoft.Extensions.Logging; -using System.Threading; -using System; -using System.IO; -using NetVips; -using System.IO.Abstractions; - -namespace API.Services.ImageConversion -{ - /// - /// Provides image conversion functionality for AVIF format using netvips library. - /// - public class AvifImageConverterProvider : IImageConverterProvider - { - private readonly ILogger _logger; - - /// - /// Gets a value indicating whether Vips supports AVIF. - /// - public bool IsVipsSupported => true; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - public AvifImageConverterProvider(ILogger logger) - { - _logger = logger; - } - - /// - /// Checks if the filename has the AVIF extension. - /// - /// The filename of the image file. - /// True if the image is AVIF image type; otherwise, false. - public bool IsSupported(string filename) - { - return filename.EndsWith(".avif", StringComparison.InvariantCultureIgnoreCase); - } - - /// - /// Converts the specified AVIF file to JPEG format. - /// - /// The filename of the AVIF file. - /// The filename of the converted JPEG file. - public string Convert(string filename) - { - string destination = Path.ChangeExtension(filename, "jpg"); - using var sourceImage = Image.NewFromFile(filename, false, Enums.Access.SequentialUnbuffered); - sourceImage.WriteToFile(destination + "[Q=99]"); - File.Delete(filename); - return destination; - } - /// - /// Creates a NetVips Image object from the specified image stream. - /// - /// The source image stream. - /// The NetVips Image object created from the image stream. - public virtual Image ImageFromStream(Stream source) - { - return Image.NewFromStream(source); - } - - /// - /// Gets the dimensions (width and height) of the specified image file. - /// - /// The filename of the image file. - /// The dimensions of the image file as a tuple of width and height. - public (int Width, int Height)? GetDimensions(string fileName) - { - using var image = Image.NewFromFile(fileName, memory: false, access: Enums.Access.SequentialUnbuffered); - return (image.Width, image.Height); - } - } -} diff --git a/API/Services/ImageConversion/HeifImageConverterProvider.cs b/API/Services/ImageConversion/HeifImageConverterProvider.cs deleted file mode 100644 index 2e5565a945..0000000000 --- a/API/Services/ImageConversion/HeifImageConverterProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.Logging; -using NetVips; -using System.IO; -using System; -using ImageMagick; - -namespace API.Services.ImageConversion -{ - /// - /// Provides image conversion functionality for HEIF and HEIC file formats using ImageMagick. - /// - public class HeifImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider - { - private readonly ILogger _logger; - - /// - /// Gets a value indicating whether Vips supports HEIF. - /// - public bool IsVipsSupported => false; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - public HeifImageConverterProvider(ILogger logger) - { - _logger = logger; - } - - /// - /// Checks if the filename has the HEIF/HEIC extension. - /// - /// The filename of the image file. - /// True if the image is HEIF/HEIC image type; otherwise, false. - public bool IsSupported(string filename) - { - return filename.EndsWith(".heif", StringComparison.InvariantCultureIgnoreCase) - || filename.EndsWith(".heic", StringComparison.InvariantCultureIgnoreCase); - } - } -} diff --git a/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs b/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs deleted file mode 100644 index 6c9e8f5ddd..0000000000 --- a/API/Services/ImageConversion/Jpeg2000ImageConverterProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Kavita.Common.EnvironmentInfo; -using Microsoft.Extensions.Logging; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using System; - -namespace API.Services.ImageConversion -{ - /// - /// Provides image conversion functionality for JPEG2000 images. - /// - public class Jpeg2000ImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider - { - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - public Jpeg2000ImageConverterProvider(ILogger logger) - { - _logger = logger; - } - - /// - /// Gets a value indicating whether Vips supports JPEG 2000. - /// - public bool IsVipsSupported => false; - - /// - /// Checks if the filename has the JPEG 2000 extension. - /// - /// The filename of the image file. - /// True if the image is JPEG 2000 image type; otherwise, false. - public bool IsSupported(string filename) - { - return filename.EndsWith(".jp2", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".j2k", StringComparison.InvariantCultureIgnoreCase); - } - } -} diff --git a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs b/API/Services/ImageConversion/JpegXLImageConverterProvider.cs deleted file mode 100644 index 60d73ce172..0000000000 --- a/API/Services/ImageConversion/JpegXLImageConverterProvider.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading; -using ImageMagick; -using Kavita.Common.EnvironmentInfo; -using Microsoft.Extensions.Logging; -using NetVips; - -namespace API.Services.ImageConversion; - -/// -/// Represents an image converter provider. -/// -public interface IImageConverterProvider -{ - /// - /// Checks if the image converter supports the image format. - /// - /// The filename of the image file. - /// True if the image converter supports the image type; otherwise, false. - bool IsSupported(string filename); - - /// - /// Converts the specified image to JPG. - /// - /// The filename of the image file. - /// The converted image file. - string Convert(string filename); - - /// - /// Gets the dimensions of the specified image file. - /// - /// The filename of the image file. - /// The dimensions of the image file as a tuple of width and height, or null if the dimensions cannot be determined. - (int Width, int Height)? GetDimensions(string fileName); - - /// - /// Gets a value indicating whether the image type supports Vips. - /// - bool IsVipsSupported { get; } - - /// - /// Creates a NetVips Image object from the specified image stream. - /// - /// The source image stream. - /// The NetVips Image object created from the image stream. - Image ImageFromStream(Stream source); -} - -public class ImageMagickConverterProvider -{ - /// - /// Converts the specified image to JPG. - /// - /// The filename of the image file. - /// The converted image file. - public virtual string Convert(string filename) - { - string destination = Path.ChangeExtension(filename, "jpg"); - using var sourceImage = new MagickImage(filename); - sourceImage.Quality = 99; - sourceImage.Write(destination); - File.Delete(filename); - return destination; - } - - /// - /// Creates a NetVips Image object from the specified image stream. - /// - /// The source image stream. - /// The NetVips Image object created from the image stream. - public virtual Image ImageFromStream(Stream source) - { - var settings = new MagickReadSettings - { - ColorSpace = ColorSpace.sRGB - }; - using var sourceImage = new MagickImage(source, settings); - float[] pixels = sourceImage.GetPixels().ToArray(); - float mul = 1F / 255F; - for (int x = 0; x < pixels.Length; x++) - pixels[x] *= mul; - GCHandle handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); - IntPtr pointer = handle.AddrOfPinnedObject(); - ulong size = (ulong)(Marshal.SizeOf() * pixels.Length); - Image im = Image.NewFromMemoryCopy(pointer, size, sourceImage.Width, sourceImage.Height,3, Enums.BandFormat.Float); - handle.Free(); - return im; - } - - /// - /// Gets the dimensions of the specified image file. - /// - /// The filename of the image file. - /// The dimensions of the image file as a tuple of width and height, or null if the dimensions cannot be determined. - public virtual (int Width, int Height)? GetDimensions(string filename) - { - var info = new MagickImageInfo(filename); - return (info.Width, info.Height); - } -} - -/// -/// Represents a JPEG-XL image converter provider. -/// -public class JpegXLImageConverterProvider : ImageMagickConverterProvider, IImageConverterProvider -{ - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - public JpegXLImageConverterProvider(ILogger logger) - { - _logger = logger; - } - - /// - /// Gets a value indicating whether Vips supports JPEG-XL. - /// - public bool IsVipsSupported => false; - - /// - /// Checks if the filename has the JPEG-XL extension. - /// - /// The filename of the image file. - /// True if the image is JPEG-XL image type; otherwise, false. - public bool IsSupported(string filename) - { - return filename.EndsWith(".jxl", StringComparison.InvariantCultureIgnoreCase); - } -} diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs index 4fa98d4e02..7345d81be4 100644 --- a/API/Services/ImageConverterService.cs +++ b/API/Services/ImageConverterService.cs @@ -4,9 +4,11 @@ using System.IO; using System.IO.Abstractions; using System.Linq; -using API.Services.ImageConversion; +using System.Text.RegularExpressions; +using API.Services.Tasks.Scanner.Parser; +using ImageMagick; using Microsoft.Extensions.Logging; -using NetVips; + namespace API.Services { @@ -15,14 +17,6 @@ namespace API.Services ///
public interface IImageConverterService { - /// - /// Converts the input stream into a Vips Image. - /// - /// The OriginalNameWithExtension of the image - /// (Not a file, only the name, if it came from the filesystem or an archive entry to identify the image type). - /// The input stream of the image. - /// NetVips Image from the input Stream. - Image GetImageFromStream(string originalNameWithExtension, Stream source); /// /// Converts the specified image file to jpg image format. @@ -32,19 +26,6 @@ public interface IImageConverterService /// The converted image file path if conversion is required. Note: original filename is deleted if conversion is executed string ConvertFile(string filename, List supportedImageFormats = null); - /// - /// Gets the dimensions (width and height) of the specified image file without read the whole image. - /// - /// The filename of the image. - /// The dimensions of the image (width and height). - (int Width, int Height)? GetDimensions(string fileName); - - /// - /// Checks if the VIPS library supports for the specified image type. - /// - /// The filename of the image. - /// True if the VIPS library support it; otherwise, false. - bool IsVipsSupported(string filename); } /// @@ -52,7 +33,7 @@ public interface IImageConverterService /// public class ImageConverterService : IImageConverterService { - private readonly IEnumerable _converters; + private readonly ILogger _logger; /// @@ -60,26 +41,14 @@ public class ImageConverterService : IImageConverterService /// /// The logger instance. /// The collection of image converter providers. - public ImageConverterService(ILogger logger, IEnumerable converters) + public ImageConverterService(ILogger logger) { _logger = logger; - _converters = converters; - } - - /// - public Image GetImageFromStream(string originalNameWithExtension, Stream source) - { - //Check if the name have one of our supported extensions. - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(originalNameWithExtension)); - - if (provider == null || provider.IsVipsSupported) return Image.NewFromStream(source); //No need to transform, Vips supports the format. - var sw = Stopwatch.StartNew(); - Image image = provider.ImageFromStream(source); - _logger.LogDebug("Image converted from '{Extension}' to '.jpg' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(originalNameWithExtension), sw.ElapsedMilliseconds); - return image; } + + private bool CheckDirectSupport(string filename, List supportedImageFormats) { if (supportedImageFormats == null) @@ -93,35 +62,21 @@ public string ConvertFile(string filename, List supportedImageFormats = { if (CheckDirectSupport(filename, supportedImageFormats)) return filename; - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); - if (provider == null) //No provider for this image type, so, is a common image format. + Match m = Regex.Match(Path.GetExtension(filename), Parser.NonUniversalFileImageExtensions, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout); + if (!m.Success) return filename; var sw = Stopwatch.StartNew(); - filename = provider.Convert(filename); + string destination = Path.ChangeExtension(filename, "jpg"); + using var sourceImage = new MagickImage(filename); + sourceImage.Quality = 99; + sourceImage.Write(destination); + File.Delete(filename); _logger.LogDebug("Image converted from '{Extension}' to '.jpg' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(filename), sw.ElapsedMilliseconds); - return filename; + return destination; } - /// - public (int Width, int Height)? GetDimensions(string fileName) - { - //Provider supported - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(fileName)); - if (provider != null) - return provider.GetDimensions(fileName); - //No provider for this image type, so, is a common image format, use netvips and original code. - using var image = Image.NewFromFile(fileName, memory: false, access: Enums.Access.SequentialUnbuffered); - return (image.Width, image.Height); - } - /// - public bool IsVipsSupported(string filename) - { - IImageConverterProvider provider = _converters.FirstOrDefault(a => a.IsSupported(filename)); - if (provider == null) - return true; - return provider.IsVipsSupported; - } + } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 56811c785d..f2e1c7256f 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -14,13 +14,9 @@ using Flurl; using Flurl.Http; using HtmlAgilityPack; +using ImageMagick; using Kavita.Common; using Microsoft.Extensions.Logging; -using NetVips; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Quantization; -using Image = NetVips.Image; namespace API.Services; #nullable enable @@ -146,13 +142,13 @@ public void ExtractImages(string? fileFilePath, string targetDirectory, int file /// /// /// - public static Enums.Size GetSizeForDimensions(Image image, int targetWidth, int targetHeight) + public static MagickGeometry GetSizeForDimensions(MagickImage image, int targetWidth, int targetHeight) { try { if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height)) { - return Enums.Size.Force; + return new MagickGeometry(targetWidth, targetHeight) { IgnoreAspectRatio = true }; } } catch (Exception) @@ -160,27 +156,27 @@ public static Enums.Size GetSizeForDimensions(Image image, int targetWidth, int /* Swallow */ } - return Enums.Size.Both; + return new MagickGeometry(targetWidth, targetHeight) { IgnoreAspectRatio = true, FillArea = true }; } - public static Enums.Interesting? GetCropForDimensions(Image image, int targetWidth, int targetHeight) + public static bool GetCropForDimensions(MagickImage image, int targetWidth, int targetHeight) { try { if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height)) { - return null; + return false; } } catch (Exception) { /* Swallow */ - return null; + return false; } - return Enums.Interesting.Attention; + return true; //crop } - public static bool WillScaleWell(Image sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) + public static bool WillScaleWell(MagickImage sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) { // Calculate the aspect ratios var sourceAspectRatio = (double) sourceImage.Width / sourceImage.Height; @@ -211,23 +207,30 @@ private static bool IsLikelyWideImage(int width, int height) return aspectRatio > 1.25; } + private MagickImage Thumbnail(string path, int width, int height) + { + var sourceImage = new MagickImage(path); + return Thumbnail(sourceImage, width, height); + } + private MagickImage Thumbnail(MagickImage sourceImage, int width, int height) + { + var geometry = GetSizeForDimensions(sourceImage, width, height); + bool crop = GetCropForDimensions(sourceImage, width, height); + sourceImage.Thumbnail(geometry); + if (crop) + sourceImage.Crop(width, height, Gravity.Center); + return sourceImage; + } public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size) { if (string.IsNullOrEmpty(path)) return string.Empty; try { - if (!_converterService.IsVipsSupported(path)) //If Vips supports the format, there is no need to convert the image, since in this case the consumer is not the browser. - path = _converterService.ConvertFile(path); - var (width, height) = size.GetDimensions(); - using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); - - using var thumbnail = Image.Thumbnail(path, width, height: height, - size: GetSizeForDimensions(sourceImage, width, height), - crop: GetCropForDimensions(sourceImage, width, height)); + using var thumbnail = Thumbnail(path, width, height); var filename = fileName + encodeFormat.GetExtension(); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } catch (Exception ex) @@ -254,15 +257,8 @@ public string WriteCoverThumbnail(string originalNameWithExtension, Stream strea var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; - using var sourceImage = _converterService.GetImageFromStream(originalNameWithExtension, stream); - - var scalingSize = GetSizeForDimensions(sourceImage, targetWidth, targetHeight); - var scalingCrop = GetCropForDimensions(sourceImage, targetWidth, targetHeight); - - using var thumbnail = sourceImage.ThumbnailImage(targetWidth, targetHeight, - size: scalingSize, - crop: scalingCrop); - + using var sourceImage = new MagickImage(stream); + using var thumbnail = Thumbnail(sourceImage, targetWidth, targetHeight); var filename = fileName + encodeFormat.GetExtension(); _directoryService.ExistOrCreate(outputDirectory); @@ -273,55 +269,37 @@ public string WriteCoverThumbnail(string originalNameWithExtension, Stream strea try { - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - + thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } - catch (VipsException) + catch (Exception) { - // NetVips Issue: https://github.com/kleisauke/net-vips/issues/234 - // Saving pdf covers from a stream can fail, so revert to old code - - if (stream.CanSeek) stream.Position = 0; - using var thumbnail2 = Image.ThumbnailStream(stream, targetWidth, height: targetHeight, - size: scalingSize, - crop: scalingCrop); - thumbnail2.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - - return filename; + return string.Empty; //IDK? } } public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - if (!_converterService.IsVipsSupported(sourceFile)) //If Vips supports the format, there is no need to convert the image, since in this case the consumer is not the browser. - sourceFile = _converterService.ConvertFile(sourceFile); - var (width, height) = size.GetDimensions(); - using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); - - using var thumbnail = Image.Thumbnail(sourceFile, width, height: height, - size: GetSizeForDimensions(sourceImage, width, height), - crop: GetCropForDimensions(sourceImage, width, height)); + using var thumbnail = Thumbnail(sourceFile, width, height); var filename = fileName + encodeFormat.GetExtension(); _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)); + thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } - public Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat) + public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat) { var file = _directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); - - using var sourceImage = Image.NewFromFile(filePath, false, Enums.Access.SequentialUnbuffered); - sourceImage.WriteToFile(outputFile); - return Task.FromResult(outputFile); + using var sourceImage = new MagickImage(filePath); + await sourceImage.WriteAsync(outputFile).ConfigureAwait(false); + return outputFile; } /// @@ -333,17 +311,16 @@ public async Task IsImage(string filePath) { try { - var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath); - if (info == null) return false; - - return true; + MagickImageInfo info = new MagickImageInfo(filePath); + if (info.Width > 0 || info.Height > 0) + return true; } catch (Exception) { /* Swallow Exception */ } - return false; + return true; } public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) @@ -418,18 +395,18 @@ public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFo .GetStreamAsync(); // Create the destination file path - using var image = Image.PngloadStream(faviconStream); + using var image = new MagickImage(faviconStream); var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); switch (encodeFormat) { case EncodeFormat.PNG: - image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename),MagickFormat.Png32).ConfigureAwait(false); break; case EncodeFormat.WEBP: - image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.WebP).ConfigureAwait(false); break; case EncodeFormat.AVIF: - image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.Avif).ConfigureAwait(false); break; default: throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); @@ -464,18 +441,18 @@ public async Task DownloadPublisherImageAsync(string publisherName, Enco .GetStreamAsync(); // Create the destination file path - using var image = Image.PngloadStream(publisherStream); + using var image = new MagickImage(publisherStream); var filename = GetPublisherFormat(publisherName, encodeFormat); switch (encodeFormat) { case EncodeFormat.PNG: - image.Pngsave(Path.Combine(_directoryService.PublisherDirectory, filename)); + await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.Png32).ConfigureAwait(false); break; case EncodeFormat.WEBP: - image.Webpsave(Path.Combine(_directoryService.PublisherDirectory, filename)); + await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.WebP).ConfigureAwait(false); break; case EncodeFormat.AVIF: - image.Heifsave(Path.Combine(_directoryService.PublisherDirectory, filename)); + await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.Avif).ConfigureAwait(false); break; default: throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); @@ -493,22 +470,20 @@ public async Task DownloadPublisherImageAsync(string publisherName, Enco private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) { - using var image = Image.NewFromFile(imagePath); - // Resize the image to speed up processing - var resizedImage = image.Resize(0.1); - - var processedImage = PreProcessImage(resizedImage); - + var settings = new MagickReadSettings { ColorSpace = ColorSpace.RGB }; + using var sourceImage = new MagickImage(imagePath, settings); + MagickImage im = new MagickImage(imagePath); + // Resize the image to speed up processing + im.Resize(new Percentage(10)); // Convert image to RGB array - var pixels = processedImage.WriteToMemory().ToArray(); - - // Convert to list of Vector3 (RGB) + float[] pixels = im.GetPixels().ToArray(); + float mul = 1F / 255F; var rgbPixels = new List(); - for (var i = 0; i < pixels.Length - 2; i += 3) - { - rgbPixels.Add(new Vector3(pixels[i], pixels[i + 1], pixels[i + 2])); - } + // Convert to list of Vector3 (RGB) + + for (int x = 0; x < pixels.Length; x += 3) + rgbPixels.Add(new Vector3(pixels[x] * mul, pixels[x+1] * mul, pixels[x + 2] * mul)); // Perform k-means clustering var clusters = KMeansClustering(rgbPixels, 4); @@ -529,7 +504,7 @@ private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) return (null, null); } - + /* private static (Vector3?, Vector3?) GetPrimaryColorSharp(string imagePath) { using var image = SixLabors.ImageSharp.Image.Load(imagePath); @@ -549,6 +524,7 @@ private static (Vector3?, Vector3?) GetPrimaryColorSharp(string imagePath) return (new Vector3(dominantColor.R, dominantColor.G, dominantColor.B), new Vector3(dominantColor.R, dominantColor.G, dominantColor.B)); } + private static Image PreProcessImage(Image image) { return image; @@ -583,7 +559,7 @@ private static Dictionary GenerateColorHistogram(Image image) return histogram; } - + */ private static bool IsColorCloseToWhiteOrBlack(Vector3 color) { var (_, _, lightness) = RgbToHsl(color); @@ -814,9 +790,12 @@ public string CreateThumbnailFromBase64(string encodedImage, string fileName, En { try { - using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); + + using var thumbnail = new MagickImage(Convert.FromBase64String(encodedImage)); + int thumbnailHeight = (int)(thumbnail.Height * ((double)thumbnailWidth / thumbnail.Width)); + thumbnail.Thumbnail(thumbnailWidth, thumbnailHeight); fileName += encodeFormat.GetExtension(); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName)); + thumbnail.Write(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName)); return fileName; } catch (Exception e) @@ -932,7 +911,7 @@ public static void CreateMergedImage(IList coverImages, CoverImageSize s } - var image = Image.Black(width, height); + var image = new MagickImage(MagickColor.FromRgb(0,0,0), width, height); var thumbnailWidth = image.Width / cols; var thumbnailHeight = image.Height / rows; @@ -940,8 +919,8 @@ public static void CreateMergedImage(IList coverImages, CoverImageSize s for (var i = 0; i < coverImages.Count; i++) { if (!File.Exists(coverImages[i])) continue; - var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential); - tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight); + var tile = new MagickImage(coverImages[i]); + tile.Thumbnail(thumbnailWidth, thumbnailHeight); var row = i / cols; var col = i % cols; @@ -954,11 +933,10 @@ public static void CreateMergedImage(IList coverImages, CoverImageSize s x = (image.Width - thumbnailWidth) / 2; y = thumbnailHeight; } - - image = image.Insert(tile, x, y); + image.Composite(tile,x,y,CompositeOperator.Over); } - image.WriteToFile(dest); + image.Write(dest); } public void UpdateColorScape(IHasCoverImage entity) diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 9e20eae994..71be6078ed 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.IO; @@ -25,7 +26,21 @@ public static class Parser public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); - public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif|\.jxl|\.heif|\.heic|\.j2k|\.jp2)"; // Don't forget to update CoverChooser + public static Dictionary> NonUniversalSupportedMimeMappings = new Dictionary>() + { + { "jp2", ["jp2", "j2k"] } , + { "j2k", ["jp2", "j2k"] } , + { "heif", ["heif", "heic"] } , + { "heic", ["heif", "heic"] } , + { "jxl", ["jxl"] } , + { "avif", ["avif"] } , + }; + public static string[] NonUniversalFileImageExtensionArray = { "avif", "jxl", "heif", "heic", "j2k", "jp2" }; + public static string[] UniversalFileImageExtensionArray = { "png", "jpeg", "jpg", "webp", "gif" }; + public static string NonUniversalFileImageExtensions = @"^(\." + string.Join(@"|\.", NonUniversalFileImageExtensionArray) + ")"; + public static string ImageFileExtensions = @"^(\." + string.Join(@"|\.", UniversalFileImageExtensionArray.Union(NonUniversalFileImageExtensionArray)) + ")"; // Don't forget to update CoverChooser + + public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; public const string EpubFileExtension = @"\.epub"; public const string PdfFileExtension = @"\.pdf"; @@ -33,7 +48,7 @@ public static class Parser private const string XmlRegexExtensions = @"\.xml"; public const string MacOsMetadataFileStartsWith = @"._"; - public const string SupportedExtensions = + public static string SupportedExtensions = ArchiveFileExtensions + "|" + ImageFileExtensions + "|" + BookFileExtensions; private const RegexOptions MatchOptions = From 10da91ce17ed832288f26663ba251d6dcd7a34a5 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 27 Sep 2024 22:38:39 -0300 Subject: [PATCH 17/37] Remove CropFromDimensions (Geometry will take care) --- API.Tests/Services/ImageServiceTests.cs | 3 --- API/Services/ImageService.cs | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index 66ef037e06..43b3addf90 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -64,10 +64,7 @@ private void GenerateFiles(string outputExtension) using var thumbnail = new MagickImage(imagePath); var size = ImageService.GetSizeForDimensions(thumbnail, dims.Width, dims.Height); - var crop = ImageService.GetCropForDimensions(thumbnail, dims.Width, dims.Height); thumbnail.Thumbnail(size); - if (crop) - thumbnail.Crop(dims.Width, dims.Height, Gravity.Center); var outputFileName = fileName + outputExtension + ".png"; thumbnail.Write(Path.Join(_testDirectory, outputFileName)); diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index f2e1c7256f..ca0422b322 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -215,10 +215,8 @@ private MagickImage Thumbnail(string path, int width, int height) private MagickImage Thumbnail(MagickImage sourceImage, int width, int height) { var geometry = GetSizeForDimensions(sourceImage, width, height); - bool crop = GetCropForDimensions(sourceImage, width, height); + sourceImage.Thumbnail(geometry); - if (crop) - sourceImage.Crop(width, height, Gravity.Center); return sourceImage; } public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size) From 1541d6ac02f21af0f482206316496287e87b1ffa Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Sat, 28 Sep 2024 00:41:18 -0300 Subject: [PATCH 18/37] BOM Changes --- API.Benchmark/ArchiveServiceBenchmark.cs | 4 +- API.Tests/Services/ArchiveServiceTests.cs | 2 +- API.Tests/Services/ImageServiceTests.cs | 4 +- API/API.csproj | 2 +- API/API.csproj.bak | 204 ++++++++++++++++++ .../ApplicationServiceExtensions.cs | 3 +- API/Extensions/FileTypeGroupExtensions.cs | 2 +- API/Services/BookService.cs | 2 +- API/Services/CacheService.cs | 30 +-- API/Services/ImageService.cs | 2 +- 10 files changed, 227 insertions(+), 28 deletions(-) create mode 100644 API/API.csproj.bak diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index f21d213473..d8e61fca4c 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using Microsoft.Extensions.Logging.Abstractions; @@ -9,7 +9,6 @@ using ImageMagick; using NSubstitute; - namespace API.Benchmark; [StopOnFirstError] @@ -22,7 +21,6 @@ public class ArchiveServiceBenchmark private readonly ArchiveService _archiveService; private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; - private const string SourceImage = "C:/Users/josep/Pictures/obey_by_grrsa-d6llkaa_colored_by_me.png"; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 9d3d06b0aa..f6e843f2df 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index 43b3addf90..58f23d4900 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using System.IO; using System.IO.Abstractions; using System.Linq; @@ -8,11 +8,9 @@ using EasyCaching.Core; using ImageMagick; using Microsoft.Extensions.Logging; - using NSubstitute; using Xunit; - namespace API.Tests.Services; public class ImageServiceTests diff --git a/API/API.csproj b/API/API.csproj index 169ddfab90..3729b96899 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -1,4 +1,4 @@ - + Default diff --git a/API/API.csproj.bak b/API/API.csproj.bak new file mode 100644 index 0000000000..169ddfab90 --- /dev/null +++ b/API/API.csproj.bak @@ -0,0 +1,204 @@ + + + + Default + net8.0 + true + Linux + true + true + ../favicon.ico + warnings + latestmajor + + + + + + + + + false + ../favicon.ico + bin\$(Configuration)\$(AssemblyName).xml + + + + bin\$(Configuration)\$(AssemblyName).xml + 1701;1702;1591 + + + + + True + $(NoWarn);1591 + + + + en + + + + + Kavita + kareadita.github.io + Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3) + + $(Configuration)-dev + + false + false + false + + False + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + Always + + + + + + + + Always + + + + + + + <_DeploymentManifestIconFile Remove="favicon.ico" /> + + + diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 2f774f3e99..8b9740577d 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,9 +1,8 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using API.Constants; using API.Data; using API.Helpers; using API.Services; - using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Metadata; diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs index 254ff400d9..cee41ca0da 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; using MimeTypes; diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 88f3cd11b6..c9eb76c755 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 82ad11b0e7..89c4ac36c8 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -240,23 +240,23 @@ public void ExtractChapterFiles(string extractPath, IReadOnlyList? fi break; case MangaFormat.Epub: case MangaFormat.Pdf: + { + if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) + { + _logger.LogError("{File} does not exist on disk", files[0].FilePath); + throw new KavitaException($"{files[0].FilePath} does not exist on disk"); + } + if (extractPdfImages) { - if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) - { - _logger.LogError("{File} does not exist on disk", files[0].FilePath); - throw new KavitaException($"{files[0].FilePath} does not exist on disk"); - } - if (extractPdfImages) - { - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); - break; - } - removeNonImages = false; - - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); break; } + removeNonImages = false; + + _directoryService.ExistOrCreate(extractPath); + _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + break; + } } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index ca0422b322..e05ec627e2 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; From 23a9aa860c21a43ada8bf3fb92b31fcfbe8f08ed Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Sat, 28 Sep 2024 00:42:06 -0300 Subject: [PATCH 19/37] Delete .bak --- API/API.csproj.bak | 204 --------------------------------------------- 1 file changed, 204 deletions(-) delete mode 100644 API/API.csproj.bak diff --git a/API/API.csproj.bak b/API/API.csproj.bak deleted file mode 100644 index 169ddfab90..0000000000 --- a/API/API.csproj.bak +++ /dev/null @@ -1,204 +0,0 @@ - - - - Default - net8.0 - true - Linux - true - true - ../favicon.ico - warnings - latestmajor - - - - - - - - - false - ../favicon.ico - bin\$(Configuration)\$(AssemblyName).xml - - - - bin\$(Configuration)\$(AssemblyName).xml - 1701;1702;1591 - - - - - True - $(NoWarn);1591 - - - - en - - - - - Kavita - kareadita.github.io - Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3) - - $(Configuration)-dev - - false - false - false - - False - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - - - Always - - - - - - - - Always - - - - - - - <_DeploymentManifestIconFile Remove="favicon.ico" /> - - - From b6a986a685fbb56844d8a11501c13eba695f84f2 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Sat, 28 Sep 2024 22:12:13 -0300 Subject: [PATCH 20/37] Bug fixes. --- API/Services/BookService.cs | 11 +++++++++-- API/Services/ImageService.cs | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index c9eb76c755..fc662b4637 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1276,10 +1276,17 @@ private static void GetPdfPage(IDocReader docReader, int pageNumber, Stream stre { using var pageReader = docReader.GetPageReader(pageNumber); var rawBytes = pageReader.GetImage(new NaiveTransparencyRemover()); - var floats = rawBytes.Select(a=> (float)a*256F).ToArray(); + var floats = new float[rawBytes.Length]; + for (var i = 0; i < rawBytes.Length; i += 4) + { + floats[i] = rawBytes[i + 2] << 8; + floats[i + 1] = rawBytes[i + 1] << 8; + floats[i+2] = rawBytes[i] << 8; + floats[i+3] = rawBytes[i + 3] << 8; + } var width = pageReader.GetPageWidth(); var height = pageReader.GetPageHeight(); - using MagickImage image = new MagickImage(rawBytes, width, height,MagickFormat.Bgra); + MagickImage image = new MagickImage(MagickColor.FromRgba(0, 0, 0,0), width, height); using var pixels = image.GetPixels(); pixels.SetArea(0,0,width, height, floats); stream.Seek(0, SeekOrigin.Begin); diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index e05ec627e2..8c31a0dba9 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -789,7 +789,7 @@ public string CreateThumbnailFromBase64(string encodedImage, string fileName, En try { - using var thumbnail = new MagickImage(Convert.FromBase64String(encodedImage)); + using var thumbnail = MagickImage.FromBase64(encodedImage); int thumbnailHeight = (int)(thumbnail.Height * ((double)thumbnailWidth / thumbnail.Width)); thumbnail.Thumbnail(thumbnailWidth, thumbnailHeight); fileName += encodeFormat.GetExtension(); From 9bf1fa8e128ce4eaafaf5748cb4c40877f2f2e50 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Tue, 1 Oct 2024 18:47:43 -0300 Subject: [PATCH 21/37] Added SmartCrop (port from smartcrop.js) --- API.Tests/Services/ImageServiceTests.cs | 7 +- API/Helpers/SmartCropHelper.cs | 546 ++++++++++++++++++++++++ API/Services/ImageService.cs | 33 +- 3 files changed, 559 insertions(+), 27 deletions(-) create mode 100644 API/Helpers/SmartCropHelper.cs diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index 58f23d4900..cd02891d9a 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -59,11 +59,8 @@ private void GenerateFiles(string outputExtension) { var fileName = Path.GetFileNameWithoutExtension(imagePath); var dims = CoverImageSize.Default.GetDimensions(); - using var thumbnail = new MagickImage(imagePath); - - var size = ImageService.GetSizeForDimensions(thumbnail, dims.Width, dims.Height); - thumbnail.Thumbnail(size); - + var thumbnail = new MagickImage(imagePath); + thumbnail = ImageService.Thumbnail(thumbnail, dims.Width, dims.Height); var outputFileName = fileName + outputExtension + ".png"; thumbnail.Write(Path.Join(_testDirectory, outputFileName)); } diff --git a/API/Helpers/SmartCropHelper.cs b/API/Helpers/SmartCropHelper.cs new file mode 100644 index 0000000000..fdeb50f2f0 --- /dev/null +++ b/API/Helpers/SmartCropHelper.cs @@ -0,0 +1,546 @@ +using System; +using System.Collections.Generic; + +using System.Threading.Tasks; +using ImageMagick; + + +namespace API.Helpers +{ + /** + * C# port of smartcrop.js + * A javascript library implementing content aware image cropping + * + * Copyright (C) 2018 Jonas Wagner + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + public static class SmartCropHelper + { + + public static MagickGeometry SmartCrop(MagickImage inputImage, int width, int height) + { + SmartCropOptions options = new SmartCropOptions + { + Width = width, + Height = height + }; + CropResult result = SmartCrop(inputImage, options); + return new MagickGeometry(result.TopCrop.X, result.TopCrop.Y, result.TopCrop.Width, result.TopCrop.Height) { FillArea = false }; + } + + + public static CropResult SmartCrop(MagickImage inputImage, SmartCropOptions options = null) + { + if (options.Aspect != 0) + { + options.Width = (int)options.Aspect; + options.Height = 1; + } + var scale = 1.0f; + var prescale = 1.0f; + + // Open the image + var image = (MagickImage)inputImage.Clone(); + if (options.Width != 0 && options.Height != 0) + { + scale = Math.Min(image.Width / options.Width, image.Height / options.Height); + options.CropWidth = (int)(options.Width * scale); + options.CropHeight = (int)(options.Height * scale); + options.MinScale = Math.Min(options.MaxScale, Math.Max(1.0f / scale, options.MinScale)); + + // Prescale if possible + if (options.Prescale) + { + prescale = Math.Min(Math.Max(256.0f / image.Width, 256.0f / image.Height), 1); + if (prescale < 1) + { + // Resample the image + image.Resize((int)(image.Width * prescale), (int)(image.Height * prescale)); + options.CropWidth = (int)(options.CropWidth * prescale); + options.CropHeight = (int)(options.CropHeight * prescale); + foreach (Boost boost in options.Boosts) + { + boost.X = (float)Math.Floor(boost.X * prescale); + boost.Y = (float)Math.Floor(boost.Y * prescale); + boost.Width = (float)Math.Floor(boost.Width * prescale); + boost.Height = (float)Math.Floor(boost.Height * prescale); + } + } + } + } + + var data = new ImageData(image); + var result = Analyse(options, data); + + // Adjust for prescale + foreach (var crop in result.Crops) + { + crop.X = (int)(crop.X / prescale); + crop.Y = (int)(crop.Y / prescale); + crop.Width = (int)(crop.Width / prescale); + crop.Height = (int)(crop.Height / prescale); + } + + return result; + } + private static CropResult Analyse(SmartCropOptions options, ImageData input) + { + CropResult result = new CropResult(); + ImageData output = new ImageData(input.Width, input.Height); + + EdgeDetect(input, output); + SkinDetect(options, input, output); + SaturationDetect(options, input, output); + ApplyBoosts(options, output); + + var scoreOutput = DownSample(output, options.ScoreDownSample); + + var topScore = double.NegativeInfinity; + Crop topCrop = null; + result.Crops = GenerateCrops(options, input.Width, input.Height); + + foreach (var crop in result.Crops) + { + crop.Score = Score(options, scoreOutput, crop); + if (crop.Score.Total > topScore) + { + topCrop = crop; + topScore = crop.Score.Total; + } + } + + result.TopCrop = topCrop; + return result; + } + private static float Thirds(double x) + { + x = (((x - 1.0 / 3.0 + 1.0) % 2.0) * 0.5 - 0.5) * 16.0; + return (float)Math.Max(1.0 - x * x, 0.0); + } + private static float Importance(SmartCropOptions options, Crop crop, double x, double y) + { + if (crop.X > x || x >= crop.X + crop.Width || crop.Y > y || y >= crop.Y + crop.Height) + { + return options.OutsideImportance; + } + + x = (x - crop.X) / crop.Width; + y = (y - crop.Y) / crop.Height; + double px = Math.Abs(0.5 - x) * 2; + double py = Math.Abs(0.5 - y) * 2; + + // Distance from edge + double dx = Math.Max(px - 1.0 + options.EdgeRadius, 0); + double dy = Math.Max(py - 1.0 + options.EdgeRadius, 0); + double d = (dx * dx + dy * dy) * options.EdgeWeight; + double s = 1.41 - Math.Sqrt(px * px + py * py); + + if (options.RuleOfThirds) + { + s += Math.Max(0, s + d + 0.5) * 1.2 * (Thirds(px) + Thirds(py)); + } + + return (float)(s + d); + } + private static ScoreResult Score(SmartCropOptions options, ImageData output, Crop crop) + { + + + var od = output.Data; + var downSample = options.ScoreDownSample; + var invDownSample = 1.0 / downSample; + var outputHeightDownSample = output.Height * downSample; + var outputWidthDownSample = output.Width * downSample; + var outputWidth = output.Width; + + double rskin = 0; + double rdetail = 0; + double rsaturation = 0; + double rboost = 0; + for (var y = 0; y < outputHeightDownSample; y += downSample) + { + for (var x = 0; x < outputWidthDownSample; x += downSample) + { + var p = (int)((Math.Floor(y * invDownSample) * outputWidth + Math.Floor(x * invDownSample)) * 4); + var i = Importance(options, crop, x, y); + var detail = od[p + 1] / 255.0; + + rskin += (od[p] / 255.0) * (detail + options.SkinBias) * i; + rdetail += detail * i; + rsaturation += (od[p + 2] / 255.0) * (detail + options.SaturationBias) * i; + rboost += (od[p + 3] / 255.0) * i; + } + } + + return new ScoreResult + { + Detail = (float)rdetail, + Saturation = (float)rsaturation, + Skin = (float)rskin, + Boost = (float)rboost, + Total = (float)((rdetail * options.DetailWeight + rskin * options.SkinWeight + + rsaturation * options.SaturationWeight + + rboost * options.BoostWeight) / + (double)(crop.Width * crop.Height)) + }; + } + private static void EdgeDetect(ImageData input, ImageData output) + { + // Get the pixel collection of both input and output images + float[] inputPixels = input.Data; + float[] outputPixels = output.Data; + + int width = input.Width; + int height = input.Height; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int p = (y * width + x) * 4; + float lightness; + + // Handle edge cases for pixels at the borders + if (x == 0 || x >= width - 1 || y == 0 || y >= height - 1) + { + lightness = Sample(inputPixels, p); + } + else + { + lightness = Sample(inputPixels, p) * 4 - + Sample(inputPixels, p - width * 4) - // Above pixel + Sample(inputPixels, p - 4) - // Left pixel + Sample(inputPixels, p + 4) - // Right pixel + Sample(inputPixels, p + width * 4); // Below pixel + } + + outputPixels[p + 1] = lightness; + } + } + } + private static List GenerateCrops(SmartCropOptions options, int width, int height) + { + var results = new List(); + var minDimension = Math.Min(width, height); + var cropWidth = options.CropWidth != 0 ? options.CropWidth : minDimension; + var cropHeight = options.CropHeight!=0 ? options.CropHeight : minDimension; + + for (var scale = options.MaxScale; scale >= options.MinScale; scale -= options.ScaleStep) + { + for (var y = 0; y + cropHeight * scale <= height; y += options.Step) + { + for (var x = 0; x + cropWidth * scale <= width; x += options.Step) + { + results.Add(new Crop + { + X = x, + Y = y, + Width = (int)Math.Round(cropWidth * scale), + Height = (int)Math.Round(cropHeight * scale) + }); + } + } + } + + return results; + } + private static void ApplyBoosts(SmartCropOptions options, ImageData output) + { + if (options.Boosts.Count==0) return; + var od = output.Data; + for (int i = 0; i < output.Width; i += 4) + { + od[i + 3] = 0; + } + foreach(Boost boost in options.Boosts) + { + ApplyBoost(boost, options, output); + } + } + private static ImageData DownSample(ImageData input, int factor) + { + var idata = input.Data; + var iwidth = input.Width; + var width = (int)Math.Floor((double)input.Width / factor); + var height = (int)Math.Floor((double)input.Height / factor); + var output = new ImageData(width, height); + var data = output.Data; + var ifactor2 = 1.0 / (factor * factor); + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var i = (y * width + x) * 4; + + double r = 0, g = 0, b = 0, a = 0; + double mr = 0, mg = 0; + + for (var v = 0; v < factor; v++) + { + for (var u = 0; u < factor; u++) + { + var j = ((y * factor + v) * iwidth + (x * factor + u)) * 4; + r += idata[j]; + g += idata[j + 1]; + b += idata[j + 2]; + a += idata[j + 3]; + mr = Math.Max(mr, idata[j]); + mg = Math.Max(mg, idata[j + 1]); + } + } + + data[i] = (byte)(r * ifactor2 * 0.5 + mr * 0.5); + data[i + 1] = (byte)(g * ifactor2 * 0.7 + mg * 0.3); + data[i + 2] = (byte)(b * ifactor2); + data[i + 3] = (byte)(a * ifactor2); + } + } + + return output; + } + private static void ApplyBoost(Boost boost, SmartCropOptions options, ImageData output) + { + var od = output.Data; + var w = output.Width; + var x0 = (int)(boost.X); + var x1 = (int)(boost.X + boost.Width); + var y0 = (int)(boost.Y); + var y1 = (int)(boost.Y + boost.Height); + var weight = boost.Weight * 255; + for (var y = y0; y < y1; y++) + { + for (var x = x0; x < x1; x++) + { + var i = (y * w + x) * 4; + od[i + 3] += weight; + } + } + } + private static void SaturationDetect(SmartCropOptions options, ImageData i, ImageData o) + { + var id = i.Data; + var od = o.Data; + var w = i.Width; + var h = i.Height; + for (var y = 0; y < h; y++) + { + for (var x = 0; x < w; x++) + { + var p = (y * w + x) * 4; + + var lightness = CIE(id[p], id[p + 1], id[p + 2]) / 255; + var sat = Saturation(id[p], id[p + 1], id[p + 2]); + + var acceptableSaturation = sat > options.SaturationThreshold; + var acceptableLightness = lightness >= options.SaturationBrightnessMin && lightness <= options.SaturationBrightnessMax; + if (acceptableLightness && acceptableSaturation) + { + od[p + 2] = (sat - options.SaturationThreshold) * (255 / (1 - options.SaturationThreshold)); + } + else + { + od[p + 2] = 0; + } + } + } + } + private static float Saturation(float r, float g, float b) + { + var maximum = Math.Max(r / 255f, Math.Max(g / 255f, b / 255f)); + var minimum = Math.Min(r / 255f, Math.Min(g / 255f, b / 255f)); + + if (Math.Abs(maximum - minimum) < float.Epsilon) + { + return 0; + } + + var l = (maximum + minimum) / 2; + var d = maximum - minimum; + + return l > 0.5f ? d / (2.0f - maximum - minimum) : d / (maximum + minimum); + } + private static void SkinDetect(SmartCropOptions options, ImageData i, ImageData o) + { + float[] id = i.Data; + float[] od = o.Data; + int w = i.Width; + int h = i.Height; + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + int p = (y * w + x) * 4; + float lightness = CIE(id[p], id[p + 1], id[p + 2]) / 255; + float skin = SkinColor(options, id[p], id[p + 1], id[p + 2]); + bool isSkinColor = skin > options.SkinThreshold; + bool isSkinBrightness = lightness >= options.SkinBrightnessMin && lightness <= options.SkinBrightnessMax; + if (isSkinColor && isSkinBrightness) + { + od[p] = (skin - options.SkinThreshold) * (255 / (1 - options.SkinThreshold)); + } + else + { + od[p] = 0; + } + } + } + } + private static float Sample(float[] data, int p) + { + return CIE(data[p], data[p + 1], data[p + 2]); + } + private static float CIE(float r, float g, float b) + { + return 0.5126f * b + 0.7152f * g + 0.0722f * r; + } + private static float SkinColor(SmartCropOptions options, float r, float g, float b) + { + double mag = Math.Sqrt(r * r + g * g + b * b); + double rd = r / mag - options.SkinColor[0]; + double gd = g / mag - options.SkinColor[1]; + double bd = b / mag - options.SkinColor[2]; + double d = Math.Sqrt(rd * rd + gd * gd + bd * bd); + return (float)(1.0 - d); + } + public class SmartCropOptions + { + public int Width { get; set; } = 0; + public int Height { get; set; } = 0; + public float Aspect { get; set; } = 0; + public int CropWidth { get; set; } = 0; + public int CropHeight { get; set; } = 0; + public float DetailWeight { get; set; } = 0.2f; + public float[] SkinColor { get; set; } = new float[] { 0.78f, 0.57f, 0.44f }; + public float SkinBias { get; set; } = 0.01f; + public float SkinBrightnessMin { get; set; } = 0.2f; + public float SkinBrightnessMax { get; set; } = 1.0f; + public float SkinThreshold { get; set; } = 0.8f; + public float SkinWeight { get; set; } = 1.8f; + public float SaturationBrightnessMin { get; set; } = 0.05f; + public float SaturationBrightnessMax { get; set; } = 0.9f; + public float SaturationThreshold { get; set; } = 0.4f; + public float SaturationBias { get; set; } = 0.2f; + public float SaturationWeight { get; set; } = 0.1f; + public int ScoreDownSample { get; set; } = 8; + public int Step { get; set; } = 8; + public float ScaleStep { get; set; } = 0.1f; + public float MinScale { get; set; } = 1.0f; + public float MaxScale { get; set; } = 1.0f; + public float EdgeRadius { get; set; } = 0.4f; + public float EdgeWeight { get; set; } = -20.0f; + public float OutsideImportance { get; set; } = -0.5f; + public float BoostWeight { get; set; } = 100.0f; + public bool RuleOfThirds { get; set; } = true; + public bool Prescale { get; set; } = true; + public List Boosts { get; set; } = new List(); + } + public class CropResult + { + public List Crops { get; set; } + public Crop TopCrop { get; set; } + } + public class ScoreResult + { + public float Detail { get; set; } + public float Saturation { get; set; } + public float Skin { get; set; } + public float Boost { get; set; } + public float Total { get; set; } + } + public class Crop + { + public int X { get; set; } + public int Y { get; set; } + public int Width { get; set; } + public int Height { get; set; } + + public ScoreResult Score { get; set; } + } + public class Boost + { + public float X { get; set; } + public float Y { get; set; } + public float Width { get; set; } + public float Height { get; set; } + public float Weight { get; set; } + } + public class ImageData + { + public int Width { get; set; } + public int Height { get; set; } + public float[] Data { get; set; } + + public ImageData(int width, int height) + { + Width = width; + Height = height; + Data = new float[width * height * 4]; + } + + public ImageData(MagickImage image) + { + Width = image.Width; + Height = image.Height; + float scale = 1.0f / 256; + if (image.ChannelCount == 4) + { + Data = image.GetPixels().GetValues(); + for (int x = 0; x < Data.Length; x++) + { + Data[x] *= scale; + } + } + else if (image.ChannelCount == 3) + { + float[] temp = image.GetPixels().GetValues(); + Data = new float[Width * Height * 4]; + int oi = 0; + int ii = 0; + for(int y = 0; y < Height*Width; y++) + { + + Data[oi++] = temp[ii++] * scale; + Data[oi++] = temp[ii++] * scale; + Data[oi++] = temp[ii++] * scale; + Data[oi++] = 255F; + } + } + else if (image.ChannelCount == 1) + { + float[] temp = image.GetPixels().GetValues(); + Data = new float[Width * Height * 4]; + int oi = 0; + int ii = 0; + for (int y = 0; y < Height * Width; y++) + { + Data[oi++] = temp[ii++] * scale; + Data[oi++] = temp[oi-1]; + Data[oi++] = temp[oi-1]; + Data[oi++] = 255F; + } + } + } + } + } +} diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 8c31a0dba9..d2d4787cbc 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -10,6 +10,7 @@ using API.Entities.Enums; using API.Entities.Interfaces; using API.Extensions; +using API.Helpers; using EasyCaching.Core; using Flurl; using Flurl.Http; @@ -17,6 +18,7 @@ using ImageMagick; using Kavita.Common; using Microsoft.Extensions.Logging; +using static API.Helpers.SmartCropHelper; namespace API.Services; #nullable enable @@ -155,26 +157,9 @@ public static MagickGeometry GetSizeForDimensions(MagickImage image, int targetW { /* Swallow */ } - - return new MagickGeometry(targetWidth, targetHeight) { IgnoreAspectRatio = true, FillArea = true }; + return SmartCropHelper.SmartCrop(image, targetWidth, targetHeight); } - public static bool GetCropForDimensions(MagickImage image, int targetWidth, int targetHeight) - { - try - { - if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height)) - { - return false; - } - } catch (Exception) - { - /* Swallow */ - return false; - } - - return true; //crop - } public static bool WillScaleWell(MagickImage sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) { @@ -207,15 +192,19 @@ private static bool IsLikelyWideImage(int width, int height) return aspectRatio > 1.25; } - private MagickImage Thumbnail(string path, int width, int height) + public static MagickImage Thumbnail(string path, int width, int height) { var sourceImage = new MagickImage(path); return Thumbnail(sourceImage, width, height); } - private MagickImage Thumbnail(MagickImage sourceImage, int width, int height) + public static MagickImage Thumbnail(MagickImage sourceImage, int width, int height) { var geometry = GetSizeForDimensions(sourceImage, width, height); - + if (geometry.Width != width && geometry.Height != height) + { + sourceImage.Crop(geometry); + geometry = new MagickGeometry(width, height) { IgnoreAspectRatio = true }; + } sourceImage.Thumbnail(geometry); return sourceImage; } @@ -476,7 +465,7 @@ private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) im.Resize(new Percentage(10)); // Convert image to RGB array float[] pixels = im.GetPixels().ToArray(); - float mul = 1F / 255F; + float mul = 1F / 256F; var rgbPixels = new List(); // Convert to list of Vector3 (RGB) From 170c3b671f8537964cbad3696534e77cbb4443bf Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 3 Oct 2024 21:03:29 -0300 Subject: [PATCH 22/37] Refactor and cleanup. --- API.Benchmark/ArchiveServiceBenchmark.cs | 27 +- API.Tests/Services/ArchiveServiceTests.cs | 17 +- API.Tests/Services/BookServiceTests.cs | 5 +- API.Tests/Services/CacheServiceTests.cs | 16 +- API.Tests/Services/ImageServiceTests.cs | 25 +- API/API.csproj | 2 +- API/Controllers/ImageController.cs | 4 +- API/Controllers/OPDSController.cs | 8 +- API/Controllers/ReaderController.cs | 10 +- API/Entities/Enums/EncodeFormat.cs | 10 +- .../ApplicationServiceExtensions.cs | 6 +- API/Extensions/EncodeFormatExtensions.cs | 3 +- API/Services/ArchiveService.cs | 6 +- API/Services/BookService.cs | 24 +- API/Services/CacheService.cs | 24 +- API/Services/ImageConverterService.cs | 82 ---- API/Services/ImageService.cs | 443 +++++++++--------- API/Services/ImageServices/IImage.cs | 102 ++++ API/Services/ImageServices/IImageFactory.cs | 67 +++ .../ImageMagick/ImageMagickImage.cs | 227 +++++++++ .../ImageMagick/ImageMagickImageFactory.cs | 79 ++++ .../ImageServices/SmartCrop.cs} | 121 ++--- API/Services/ReadingListService.cs | 4 +- docker-build.sh | 2 +- 24 files changed, 838 insertions(+), 476 deletions(-) delete mode 100644 API/Services/ImageConverterService.cs create mode 100644 API/Services/ImageServices/IImage.cs create mode 100644 API/Services/ImageServices/IImageFactory.cs create mode 100644 API/Services/ImageServices/ImageMagick/ImageMagickImage.cs create mode 100644 API/Services/ImageServices/ImageMagick/ImageMagickImageFactory.cs rename API/{Helpers/SmartCropHelper.cs => Services/ImageServices/SmartCrop.cs} (83%) diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index d8e61fca4c..e975bd00f4 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,14 +1,17 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; +using API.Entities.Enums; using Microsoft.Extensions.Logging.Abstractions; using API.Services; +using API.Services.ImageServices; +using API.Services.ImageServices.ImageMagick; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; using EasyCaching.Core; -using ImageMagick; using NSubstitute; + namespace API.Benchmark; [StopOnFirstError] @@ -21,14 +24,16 @@ public class ArchiveServiceBenchmark private readonly ArchiveService _archiveService; private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; + private readonly IImageFactory _imageFactory; private const string SourceImage = "C:/Users/josep/Pictures/obey_by_grrsa-d6llkaa_colored_by_me.png"; public ArchiveServiceBenchmark() { _directoryService = new DirectoryService(null, new FileSystem()); - _imageService = new ImageService(null, _directoryService, Substitute.For(), Substitute.For()); + _imageService = new ImageService(null, _directoryService, Substitute.For(), Substitute.For()); _archiveService = new ArchiveService(new NullLogger(), _directoryService, _imageService, Substitute.For()); + _imageFactory = new ImageMagickImageFactory(); } [Benchmark(Baseline = true)] @@ -62,11 +67,11 @@ public void ImageSharp_ExtractImage_PNG() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail2 = new MagickImage(stream); + using var thumbnail2 = _imageFactory.Create(stream); int width = 320; int height = (int)(thumbnail2.Height * (width / (double)thumbnail2.Width)); thumbnail2.Thumbnail(width, height); - thumbnail2.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png")); + thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png"), EncodeFormat.PNG, 100); } [Benchmark] @@ -76,11 +81,11 @@ public void ImageSharp_ExtractImage_WebP() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail2 = new MagickImage(stream); + using var thumbnail2 = _imageFactory.Create(stream); int width = 320; int height = (int)(thumbnail2.Height * (width / (double)thumbnail2.Width)); thumbnail2.Thumbnail(width, height); - thumbnail2.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp")); + thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp"), EncodeFormat.PNG, 100); } [Benchmark] @@ -90,11 +95,11 @@ public void NetVips_ExtractImage_PNG() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail = new MagickImage(stream); + using var thumbnail = _imageFactory.Create(stream); int width = 320; int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); thumbnail.Thumbnail(width, height); - thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.png")); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.png"), EncodeFormat.PNG, 100); } [Benchmark] @@ -104,11 +109,11 @@ public void NetVips_ExtractImage_WebP() _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail = new MagickImage(stream); + using var thumbnail = _imageFactory.Create(stream); int width = 320; int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); thumbnail.Thumbnail(width, height); - thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.webp")); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.webp"), EncodeFormat.WEBP, 100); } // Benchmark to test default GetNumberOfPages from archive diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index f6e843f2df..7f0a9b7b7b 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; @@ -7,8 +7,9 @@ using API.Archive; using API.Entities.Enums; using API.Services; +using API.Services.ImageServices; +using API.Services.ImageServices.ImageMagick; using EasyCaching.Core; -using ImageMagick; using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.Extensions; @@ -29,7 +30,7 @@ public ArchiveServiceTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _archiveService = new ArchiveService(_logger, _directoryService, - new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()), + new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()), Substitute.For()); } @@ -167,16 +168,16 @@ public void FindFirstEntry(string[] files, string expected) public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile) { var ds = Substitute.For(_directoryServiceLogger, new FileSystem()); - var imageService = new ImageService(Substitute.For>(), ds, Substitute.For(), Substitute.For()); + var imageService = new ImageService(Substitute.For>(), ds, Substitute.For(), new ImageMagickImageFactory()); var archiveService = Substitute.For(_logger, ds, imageService, Substitute.For()); var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")); - using var thumbnail = new MagickImage(Path.Join(testDirectory, expectedOutputFile)); + using var thumbnail = imageService.ImageFactory.Create(Path.Join(testDirectory, expectedOutputFile)); int width = 320; int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); thumbnail.Thumbnail(width, height); using MemoryStream stream = new MemoryStream(); - thumbnail.Write(stream, MagickFormat.Png32); + thumbnail.Save(stream, EncodeFormat.PNG, 100); var expectedBytes = stream.ToArray(); archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default); @@ -204,7 +205,7 @@ public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFi [InlineData("sorting.zip", "sorting.expected.png")] public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile) { - var imageService = new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()); + var imageService = new ImageService(Substitute.For>(), _directoryService, Substitute.For(), Substitute.For()); var archiveService = Substitute.For(_logger, new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService, Substitute.For()); @@ -230,7 +231,7 @@ public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOu public void CanParseCoverImage(string inputFile) { var imageService = Substitute.For(); - imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index b24085bded..48245e6392 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,6 +1,7 @@ -using System.IO; +using System.IO; using System.IO.Abstractions; using API.Services; +using API.Services.ImageServices; using EasyCaching.Core; using Microsoft.Extensions.Logging; using NSubstitute; @@ -17,7 +18,7 @@ public BookServiceTests() { var directoryService = new DirectoryService(Substitute.For>(), new FileSystem()); _bookService = new BookService(_logger, directoryService, - new ImageService(Substitute.For>(), directoryService, Substitute.For(), Substitute.For()) + new ImageService(Substitute.For>(), directoryService, Substitute.For(), Substitute.For()) , Substitute.For()); } diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 45a86d04f3..2bc18f8b60 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; @@ -158,7 +158,7 @@ public async Task Ensure_DirectoryAlreadyExists_DontExtractAnything() new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For(), Substitute.For()); await ResetDB(); var s = new SeriesBuilder("Test").Build(); @@ -234,7 +234,7 @@ public void CleanupChapters_AllFilesShouldBeDeleted() var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For(), Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); @@ -256,7 +256,7 @@ public void GetCachedEpubFile_ShouldReturnFirstEpub() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For(), Substitute.For()); var c = new ChapterBuilder("1") .WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build()) @@ -297,7 +297,7 @@ public void GetCachedPagePath_ReturnNullIfNoFiles() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(),Substitute.For()); + Substitute.For(),Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -341,7 +341,7 @@ public void GetCachedPagePath_GetFileFromFirstFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -382,7 +382,7 @@ public void GetCachedPagePath_GetLastPageFromSingleFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -427,7 +427,7 @@ public void GetCachedPagePath_GetFileFromSecondFile() var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For(), Substitute.For()); + Substitute.For(), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index cd02891d9a..3f63228e6c 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -1,12 +1,12 @@ -using System.Drawing; +using System.Drawing; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Text; using API.Entities.Enums; using API.Services; +using API.Services.ImageServices.ImageMagick; using EasyCaching.Core; -using ImageMagick; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -55,14 +55,15 @@ private void GenerateFiles(string outputExtension) .ToList(); // Step 3: Process each image + ImageMagickImageFactory factory = new ImageMagickImageFactory(); foreach (var imagePath in imageFiles) { var fileName = Path.GetFileNameWithoutExtension(imagePath); var dims = CoverImageSize.Default.GetDimensions(); - var thumbnail = new MagickImage(imagePath); + var thumbnail = factory.Create(imagePath); thumbnail = ImageService.Thumbnail(thumbnail, dims.Width, dims.Height); var outputFileName = fileName + outputExtension + ".png"; - thumbnail.Write(Path.Join(_testDirectory, outputFileName)); + thumbnail.Save(Path.Join(_testDirectory, outputFileName), EncodeFormat.PNG,100); } } @@ -72,6 +73,7 @@ private void GenerateHtmlFile() .Where(file => !file.EndsWith("html")) .Where(file => !file.Contains(OutputPattern) && !file.Contains(BaselinePattern)) .ToList(); + ImageMagickImageFactory factory = new ImageMagickImageFactory(); var htmlBuilder = new StringBuilder(); htmlBuilder.AppendLine(""); @@ -97,7 +99,7 @@ private void GenerateHtmlFile() var outputPath = Path.Combine(_testDirectory, fileName + "_output.png"); var dims = CoverImageSize.Default.GetDimensions(); - using var sourceImage = new MagickImage(imagePath); + using var sourceImage = factory.Create(imagePath); htmlBuilder.AppendLine("
"); htmlBuilder.AppendLine($"

{fileName} ({((double) sourceImage.Width / sourceImage.Height).ToString("F2")}) - {ImageService.WillScaleWell(sourceImage, dims.Width, dims.Height)}

"); htmlBuilder.AppendLine($"\"{fileName}\""); @@ -136,11 +138,16 @@ public void TestColorScapes() .Where(file => !file.Contains(OutputPattern) && !file.Contains(BaselinePattern)) .ToList(); + var factory = LoggerFactory.Create(builder => { builder.AddConsole(); }); + var logger = factory.CreateLogger(); + + ImageService service = new ImageService(logger, null, null, new ImageMagickImageFactory()); + // Step 3: Process each image foreach (var imagePath in imageFiles) { var fileName = Path.GetFileNameWithoutExtension(imagePath); - var colors = ImageService.CalculateColorScape(imagePath); + var colors = service.CalculateColorScape(imagePath); // Generate primary color image GenerateColorImage(colors.Primary, Path.Combine(_testDirectoryColorScapes, $"{fileName}_primary_output.png")); @@ -156,10 +163,10 @@ public void TestColorScapes() private static void GenerateColorImage(string hexColor, string outputPath) { + ImageMagickImageFactory factory = new ImageMagickImageFactory(); var color = ImageService.HexToRgb(hexColor); - using var colorImage = - new MagickImage(MagickColor.FromRgb(color.R, color.G, color.B), 200, 100); - colorImage.Write(outputPath); + using var colorImage = factory.Create(200,100,color.R, color.G, color.B); + colorImage.Save(outputPath, EncodeFormat.PNG, 100); } private void GenerateHtmlFileForColorScape() diff --git a/API/API.csproj b/API/API.csproj index 3729b96899..169ddfab90 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -1,4 +1,4 @@ - + Default diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index 6275c6d4cd..5a03798793 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -171,7 +171,7 @@ private async Task GenerateCollectionCoverImage(int collectionId) var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); destFile += settings.EncodeMediaAs.GetExtension(); if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; - ImageService.CreateMergedImage( + _imageService.CreateMergedImage( covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), settings.CoverImageSize, destFile); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 1ef56b8bfb..dd5aaf2a74 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -43,7 +43,7 @@ public class OpdsController : BaseApiController private readonly ISeriesService _seriesService; private readonly IAccountService _accountService; private readonly ILocalizationService _localizationService; - private readonly IImageConverterService _converterService; + private readonly IImageService _imageService; private readonly IMapper _mapper; @@ -82,7 +82,7 @@ public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService, ICacheService cacheService, IReaderService readerService, ISeriesService seriesService, IAccountService accountService, ILocalizationService localizationService, - IImageConverterService converterService, + IImageService imageService, IMapper mapper) { _unitOfWork = unitOfWork; @@ -93,7 +93,7 @@ public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, _seriesService = seriesService; _accountService = accountService; _localizationService = localizationService; - _converterService = converterService; + _imageService = imageService; _mapper = mapper; _xmlSerializer = new XmlSerializer(typeof(Feed)); @@ -1245,7 +1245,7 @@ public async Task GetPageStreamedImage(string apiKey, [FromQuery] var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); - path = _converterService.ConvertFile(path, Request.SupportedImageTypesFromRequest()); + path = _imageService.ReplaceImageFileFormat(path, Request.SupportedImageTypesFromRequest()); var content = await _directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 973085c026..e8ad2d8ba9 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -42,7 +42,7 @@ public class ReaderController : BaseApiController private readonly IEventHub _eventHub; private readonly IScrobblingService _scrobblingService; private readonly ILocalizationService _localizationService; - private readonly IImageConverterService _converterService; + private readonly IImageService _imageService; /// public ReaderController(ICacheService cacheService, @@ -51,7 +51,7 @@ public ReaderController(ICacheService cacheService, IAccountService accountService, IEventHub eventHub, IScrobblingService scrobblingService, ILocalizationService localizationService, - IImageConverterService converterService) + IImageService imageService) { _cacheService = cacheService; _unitOfWork = unitOfWork; @@ -62,7 +62,7 @@ public ReaderController(ICacheService cacheService, _eventHub = eventHub; _scrobblingService = scrobblingService; _localizationService = localizationService; - _converterService = converterService; + _imageService = imageService; } /// @@ -124,7 +124,7 @@ public async Task GetImage(int chapterId, int page, string apiKey, var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); - path = _converterService.ConvertFile(path, Request.SupportedImageTypesFromRequest()); + path = _imageService.ReplaceImageFileFormat(path, Request.SupportedImageTypesFromRequest()); var format = Path.GetExtension(path); return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path), true); @@ -188,7 +188,7 @@ public async Task GetBookmarkImage(int seriesId, string apiKey, in { var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); - path = _converterService.ConvertFile(path, Request.SupportedImageTypesFromRequest()); + path = _imageService.ReplaceImageFileFormat(path, Request.SupportedImageTypesFromRequest()); var format = Path.GetExtension(path); return PhysicalFile(path, format.GetMimeType(), Path.GetFileName(path)); diff --git a/API/Entities/Enums/EncodeFormat.cs b/API/Entities/Enums/EncodeFormat.cs index 70345f1dbb..189b127458 100644 --- a/API/Entities/Enums/EncodeFormat.cs +++ b/API/Entities/Enums/EncodeFormat.cs @@ -1,13 +1,17 @@ -using System.ComponentModel; +using System.ComponentModel; namespace API.Entities.Enums; -public enum EncodeFormat +public enum EncodeFormat { [Description("PNG")] PNG = 0, [Description("WebP")] WEBP = 1, [Description("AVIF")] - AVIF = 2 + AVIF = 2, + //Internal Use + [Description("JPEG")] + JPEG = 3 } + diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 8b9740577d..cbcad82063 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,8 +1,10 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using API.Constants; using API.Data; using API.Helpers; using API.Services; +using API.Services.ImageServices; +using API.Services.ImageServices.ImageMagick; using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Metadata; @@ -34,7 +36,7 @@ public static void AddApplicationServices(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Extensions/EncodeFormatExtensions.cs b/API/Extensions/EncodeFormatExtensions.cs index 924ae8b891..301c69fa5f 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/API/Extensions/EncodeFormatExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using API.Entities.Enums; namespace API.Extensions; @@ -13,6 +13,7 @@ public static string GetExtension(this EncodeFormat encodeFormat) EncodeFormat.PNG => ".png", EncodeFormat.WEBP => ".webp", EncodeFormat.AVIF => ".avif", + EncodeFormat.JPEG => ".jpg", _ => throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null) }; } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 5ce662bedc..46ea4c3018 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -233,7 +233,7 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entry = archive.Entries.Single(e => e.FullName == entryName); using var stream = entry.Open(); - return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: { @@ -244,7 +244,7 @@ public string GetCoverImage(string archivePath, string fileName, string outputDi var entry = archive.Entries.Single(e => e.Key == entryName); using var stream = entry.OpenEntryStream(); - return _imageService.WriteCoverThumbnail(entryName, stream, fileName, outputDirectory, format, size); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index fc662b4637..27096f9cb1 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,6 +11,7 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; +using API.Services.ImageServices; using API.Services.Tasks.Scanner.Parser; using Docnet.Core; using Docnet.Core.Converters; @@ -18,7 +19,6 @@ using Docnet.Core.Readers; using ExCSS; using HtmlAgilityPack; -using ImageMagick; using Kavita.Common; using Microsoft.Extensions.Logging; using Microsoft.IO; @@ -1228,7 +1228,7 @@ public string GetCoverImage(string fileFilePath, string fileName, string outputD if (coverImageContent == null) return string.Empty; using var stream = coverImageContent.GetContentStream(); - return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { @@ -1251,7 +1251,7 @@ private string GetPdfCoverImage(string fileFilePath, string fileName, string out using var stream = StreamManager.GetStream("BookService.GetPdfPage"); GetPdfPage(docReader, 0, stream); - return _imageService.WriteCoverThumbnail(fileFilePath, stream, fileName, outputDirectory, encodeFormat, size); + return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) @@ -1272,25 +1272,15 @@ private string GetPdfCoverImage(string fileFilePath, string fileName, string out /// /// /// - private static void GetPdfPage(IDocReader docReader, int pageNumber, Stream stream) + private void GetPdfPage(IDocReader docReader, int pageNumber, Stream stream) { using var pageReader = docReader.GetPageReader(pageNumber); var rawBytes = pageReader.GetImage(new NaiveTransparencyRemover()); - var floats = new float[rawBytes.Length]; - for (var i = 0; i < rawBytes.Length; i += 4) - { - floats[i] = rawBytes[i + 2] << 8; - floats[i + 1] = rawBytes[i + 1] << 8; - floats[i+2] = rawBytes[i] << 8; - floats[i+3] = rawBytes[i + 3] << 8; - } var width = pageReader.GetPageWidth(); var height = pageReader.GetPageHeight(); - MagickImage image = new MagickImage(MagickColor.FromRgba(0, 0, 0,0), width, height); - using var pixels = image.GetPixels(); - pixels.SetArea(0,0,width, height, floats); + IImage image = _imageService.ImageFactory.CreateFromBGRAByteArray(rawBytes, width, height); stream.Seek(0, SeekOrigin.Begin); - image.Write(stream, MagickFormat.Png); + image.Save(stream, EncodeFormat.PNG, 100); stream.Seek(0, SeekOrigin.Begin); } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 89c4ac36c8..6c0fe17a9f 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -11,7 +11,6 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; -using ImageMagick; using Kavita.Common; using Microsoft.Extensions.Logging; using static System.Net.Mime.MediaTypeNames; @@ -53,20 +52,21 @@ public class CacheService : ICacheService private readonly IDirectoryService _directoryService; private readonly IReadingItemService _readingItemService; private readonly IBookmarkService _bookmarkService; - private readonly IImageConverterService _converterService; + private readonly IImageService _imageService; + private static readonly ConcurrentDictionary ExtractLocks = new(); public CacheService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IReadingItemService readingItemService, - IBookmarkService bookmarkService, IImageConverterService converterService) + IBookmarkService bookmarkService, IImageService imageService) { _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; _readingItemService = readingItemService; _bookmarkService = bookmarkService; - _converterService = converterService; + _imageService = imageService; } public IEnumerable GetCachedPages(int chapterId) @@ -99,14 +99,18 @@ public IEnumerable GetCachedFileDimensions(string cachePath) for (var i = 0; i < files.Length; i++) { var file = files[i]; - MagickImageInfo info = new MagickImageInfo(file); - + var info = _imageService.ImageFactory.GetDimensions(file); + if (info == null) + { + _logger.LogError("There was an error calculating image dimensions for image {file}", file); + continue; + } dimensions.Add(new FileDimensionDto() { PageNumber = i, - Height = info.Height, - Width = info.Width, - IsWide = info.Width > info.Height, + Height = info.Value.Height, + Width = info.Value.Width, + IsWide = info.Value.Width > info.Value.Height, FileName = file.Replace(cachePath, string.Empty) }); } diff --git a/API/Services/ImageConverterService.cs b/API/Services/ImageConverterService.cs deleted file mode 100644 index 7345d81be4..0000000000 --- a/API/Services/ImageConverterService.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Text.RegularExpressions; -using API.Services.Tasks.Scanner.Parser; -using ImageMagick; -using Microsoft.Extensions.Logging; - - -namespace API.Services -{ - /// - /// Represents an image converter service. - /// - public interface IImageConverterService - { - - /// - /// Converts the specified image file to jpg image format. - /// - /// The filename of the image. - /// The list of supported image formats by the client from the Accept Header. If null is passed, the conversion will execute if the image requires conversion. - /// The converted image file path if conversion is required. Note: original filename is deleted if conversion is executed - string ConvertFile(string filename, List supportedImageFormats = null); - - } - - /// - /// Represents an implementation of the image converter service. - /// - public class ImageConverterService : IImageConverterService - { - - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// The collection of image converter providers. - public ImageConverterService(ILogger logger) - { - _logger = logger; - - } - - - - private bool CheckDirectSupport(string filename, List supportedImageFormats) - { - if (supportedImageFormats == null) - return false; - string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); - return supportedImageFormats.Contains(ext); - } - - /// - public string ConvertFile(string filename, List supportedImageFormats = null) - { - if (CheckDirectSupport(filename, supportedImageFormats)) - return filename; - Match m = Regex.Match(Path.GetExtension(filename), Parser.NonUniversalFileImageExtensions, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout); - if (!m.Success) - return filename; - var sw = Stopwatch.StartNew(); - string destination = Path.ChangeExtension(filename, "jpg"); - using var sourceImage = new MagickImage(filename); - sourceImage.Quality = 99; - sourceImage.Write(destination); - File.Delete(filename); - _logger.LogDebug("Image converted from '{Extension}' to '.jpg' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(filename), sw.ElapsedMilliseconds); - return destination; - } - - - - - } -} diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index d2d4787cbc..d20f8584f0 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,81 +1,164 @@ -using System; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Numerics; +using System.Text.RegularExpressions; using System.Threading.Tasks; using API.Constants; using API.DTOs; using API.Entities.Enums; using API.Entities.Interfaces; using API.Extensions; -using API.Helpers; +using API.Services.ImageServices; +using API.Services.Tasks.Scanner.Parser; using EasyCaching.Core; using Flurl; using Flurl.Http; using HtmlAgilityPack; -using ImageMagick; using Kavita.Common; +using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.Extensions.Logging; -using static API.Helpers.SmartCropHelper; -namespace API.Services; -#nullable enable +namespace API.Services; +/// +/// Interface for the ImageService. +/// public interface IImageService { + /// + /// Extracts images from a file and copies them to the target directory. + /// + /// The file path of the source file. + /// The target directory to copy the images to. + /// The number of files to extract. Default is 1. void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); - string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size); /// - /// Creates a Thumbnail version of a base64 image + /// Gets the cover image from the specified path and saves it to the output directory. /// - /// base64 encoded image - /// - /// Convert and save as encoding format - /// Width of thumbnail + /// The path of the image file. + /// The name of the output file. + /// The output directory to save the image. + /// The encoding format to convert and save the image. + /// The size of the cover image. + /// The quality of the image. Default is 100. + /// The file name with extension of the saved image. + string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100); + + /// + /// Creates a thumbnail version of a base64 encoded image. + /// + /// The base64 encoded image. + /// The name of the output file. + /// The encoding format to convert and save the image. + /// The width of the thumbnail. Default is 320. + /// The quality of the image. Default is 100. + /// The file name with extension of the saved thumbnail image. + string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, int quality = 100); + + /// + /// Creates a thumbnail out of a memory stream and saves to with the passed + /// fileName and the appropriate 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 the passed encoding + /// The size of the cover image. + /// The quality of the image. Default is 100. /// File name with extension of the file. This will always write to - string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320); + string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); + /// - /// Writes out a thumbnail by stream input + /// Writes out a thumbnail image from a file path input and saves to with the passed /// - /// - /// - /// - /// - /// - string WriteCoverThumbnail(string originalNameWithExtension, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + /// The path of the source image file. + /// filename to save as without extension + /// Where to output the file, defaults to covers directory + /// Export the file as the passed encoding + /// The size of the cover image. + /// The quality of the image. Default is 100. + /// File name with extension of the file. This will always write to + string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); + /// - /// Writes out a thumbnail by file path input + /// Converts the specified image file to the specified encoding format and saves it to the output path. /// - /// - /// - /// - /// - /// - string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + /// The full path of the image file to convert. + /// The path to save the converted image file. + /// The encoding format to convert the image. + /// The quality of the image. Default is 100. + /// The file path of the converted image file. + Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100); + /// - /// Converts the passed image to encoding and outputs it in the same directory + /// Checks if the specified file is an image. /// - /// Full path to the image to convert - /// Where to output the file - /// Encoding Format - /// File of written encoded image - Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat); + /// The path of the file to check. + /// True if the file is an image; otherwise, false. Task IsImage(string filePath); - Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat); - Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat); + + /// + /// Downloads the favicon from the specified URL and saves it to the output directory. + /// + /// The URL of the favicon. + /// The encoding format to convert and save the favicon. + /// The quality of the favicon. Default is 100. + /// The file name with extension of the saved favicon. + Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100); + + /// + /// Downloads the publisher image for the specified publisher name and saves it to the output directory. + /// + /// The name of the publisher. + /// The encoding format to convert and save the publisher image. + /// The quality of the publisher image. Default is 100. + /// The file name with extension of the saved publisher image. + Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100); + + /// + /// Updates the color scape of an entity with a cover image. + /// + /// The entity with a cover image. void UpdateColorScape(IHasCoverImage entity); + + /// + /// Replaces the file format of an image with the specified supported image formats by the browser, if needed, otherwise original file will be served. + /// + /// The name of the image file. + /// The list of supported image formats by the browser. + /// The encoding format to convert the image. Default is JPG + /// The quality of the image. Default is 99. + /// The file name with extension of the replaced image file. + string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99); + + /// + /// Creates a merged image from a list of cover images and saves it to the specified destination. + /// + /// The list of cover images. + /// The size of the merged image. + /// The destination path to save the merged image. + /// The encoding format to convert and save the merged image. Default is PNG. + /// The quality of the merged image. Default is 100. + void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100); + + /// + /// The image factory used to create and manipulate images. + /// + IImageFactory ImageFactory { get; } } public class ImageService : IImageService { - public const string Name = "BookmarkService"; + public const string Name = "ImageService"; private readonly ILogger _logger; private readonly IDirectoryService _directoryService; private readonly IEasyCachingProviderFactory _cacheFactory; - private readonly IImageConverterService _converterService; + private readonly IImageFactory _imageFactory; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"series\d+"; public const string CollectionTagCoverImageRegex = @"tag\d+"; @@ -114,14 +197,24 @@ public class ImageService : IImageService ["https://app.plex.tv"] = "https://plex.tv" }; - public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory, IImageConverterService converterService) + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The directory service. + /// The cache factory. + /// The image factory. + public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory, IImageFactory imageFactory) { _logger = logger; _directoryService = directoryService; _cacheFactory = cacheFactory; - _converterService = converterService; + _imageFactory = imageFactory; } + public IImageFactory ImageFactory => _imageFactory; + + /// public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) { if (string.IsNullOrEmpty(fileFilePath)) return; @@ -136,32 +229,39 @@ public void ExtractImages(string? fileFilePath, string targetDirectory, int file Tasks.Scanner.Parser.Parser.ImageFileExtensions); } } - + /// - /// Tries to determine if there is a better mode for resizing + /// Creates a thumbnail image from the specified image with the given width and height. + /// If the image aspect ratio is significantly different from the target aspect ratio, it will be context aware cropped to fit. /// - /// - /// - /// - /// - public static MagickGeometry GetSizeForDimensions(MagickImage image, int targetWidth, int targetHeight) + /// The image to create a thumbnail from. + /// The width of the thumbnail. + /// The height of the thumbnail. + /// The thumbnail image. + public static IImage Thumbnail(IImage image, int width, int height) { try { - if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height)) + if (WillScaleWell(image, width, height) || IsLikelyWideImage(image.Width, image.Height)) { - return new MagickGeometry(targetWidth, targetHeight) { IgnoreAspectRatio = true }; + image.Thumbnail(width, height); + return image; } } catch (Exception) { /* Swallow */ } - return SmartCropHelper.SmartCrop(image, targetWidth, targetHeight); + var crop = SmartCrop.Crop(image, new SmartCrop.SmartCropOptions { Width = width, Height = height }); + if (crop.TopCrop.Width != width && crop.TopCrop.Height != height) + { + image.Crop(crop.TopCrop.X, crop.TopCrop.Y, crop.TopCrop.Width, crop.TopCrop.Height); + } + image.Thumbnail(width, height); + return image; } - - public static bool WillScaleWell(MagickImage sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) + public static bool WillScaleWell(IImage sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) { // Calculate the aspect ratios var sourceAspectRatio = (double) sourceImage.Width / sourceImage.Height; @@ -192,32 +292,18 @@ private static bool IsLikelyWideImage(int width, int height) return aspectRatio > 1.25; } - public static MagickImage Thumbnail(string path, int width, int height) - { - var sourceImage = new MagickImage(path); - return Thumbnail(sourceImage, width, height); - } - public static MagickImage Thumbnail(MagickImage sourceImage, int width, int height) - { - var geometry = GetSizeForDimensions(sourceImage, width, height); - if (geometry.Width != width && geometry.Height != height) - { - sourceImage.Crop(geometry); - geometry = new MagickGeometry(width, height) { IgnoreAspectRatio = true }; - } - sourceImage.Thumbnail(geometry); - return sourceImage; - } - public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size) + + /// + public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100) { if (string.IsNullOrEmpty(path)) return string.Empty; try { var (width, height) = size.GetDimensions(); - using var thumbnail = Thumbnail(path, width, height); + using var thumbnail = Thumbnail(_imageFactory.Create(path), width, height); var filename = fileName + encodeFormat.GetExtension(); - thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); return filename; } catch (Exception ex) @@ -228,24 +314,13 @@ public string GetCoverImage(string path, string fileName, string outputDirectory return string.Empty; } - /// - /// Creates a thumbnail out of a memory stream and saves to with the passed - /// fileName and the appropriate extension. - /// - /// The OriginalNameWithExtension of the image - /// (Not a file, only the name, if it came from the filesystem or an archive entry). - /// 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 the passed encoding - /// File name with extension of the file. This will always write to - public string WriteCoverThumbnail(string originalNameWithExtension, Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) + /// + public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) { var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; - using var sourceImage = new MagickImage(stream); - using var thumbnail = Thumbnail(sourceImage, targetWidth, targetHeight); + using var thumbnail = Thumbnail(_imageFactory.Create(stream), targetWidth, targetHeight); var filename = fileName + encodeFormat.GetExtension(); _directoryService.ExistOrCreate(outputDirectory); @@ -256,7 +331,7 @@ public string WriteCoverThumbnail(string originalNameWithExtension, Stream strea try { - thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); return filename; } catch (Exception) @@ -264,53 +339,49 @@ public string WriteCoverThumbnail(string originalNameWithExtension, Stream strea return string.Empty; //IDK? } } - - public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) + /// + public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) { var (width, height) = size.GetDimensions(); - using var thumbnail = Thumbnail(sourceFile, width, height); + using var thumbnail = Thumbnail(_imageFactory.Create(sourceFile), width, height); var filename = fileName + encodeFormat.GetExtension(); _directoryService.ExistOrCreate(outputDirectory); try { _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} - thumbnail.Write(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); return filename; } - - public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat) + /// + public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100) { var file = _directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); - using var sourceImage = new MagickImage(filePath); - await sourceImage.WriteAsync(outputFile).ConfigureAwait(false); + using var sourceImage = _imageFactory.Create(filePath); + await sourceImage.SaveAsync(outputFile, encodeFormat, quality).ConfigureAwait(false); return outputFile; } - /// - /// Performs I/O to determine if the file is a valid Image - /// - /// - /// - public async Task IsImage(string filePath) + /// + public Task IsImage(string filePath) { try { - MagickImageInfo info = new MagickImageInfo(filePath); - if (info.Width > 0 || info.Height > 0) - return true; + var result= _imageFactory.GetDimensions(filePath); + if (result!=null) + return Task.FromResult(true); } catch (Exception) { /* Swallow Exception */ } - return true; + return Task.FromResult(false); } - - public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) + /// + public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100) { // Parse the URL to get the domain (including subdomain) var uri = new Uri(url); @@ -382,24 +453,9 @@ public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFo .GetStreamAsync(); // Create the destination file path - using var image = new MagickImage(faviconStream); + using var image = _imageFactory.Create(faviconStream); var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); - switch (encodeFormat) - { - case EncodeFormat.PNG: - await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename),MagickFormat.Png32).ConfigureAwait(false); - break; - case EncodeFormat.WEBP: - await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.WebP).ConfigureAwait(false); - break; - case EncodeFormat.AVIF: - await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.Avif).ConfigureAwait(false); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } - - + await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); _logger.LogDebug("Favicon for {Domain} downloaded and saved successfully", domain); return filename; } catch (Exception ex) @@ -408,8 +464,8 @@ public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFo throw; } } - - public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat) + /// + public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100) { try { @@ -428,24 +484,9 @@ public async Task DownloadPublisherImageAsync(string publisherName, Enco .GetStreamAsync(); // Create the destination file path - using var image = new MagickImage(publisherStream); + using var image = _imageFactory.Create(publisherStream); var filename = GetPublisherFormat(publisherName, encodeFormat); - switch (encodeFormat) - { - case EncodeFormat.PNG: - await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.Png32).ConfigureAwait(false); - break; - case EncodeFormat.WEBP: - await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.WebP).ConfigureAwait(false); - break; - case EncodeFormat.AVIF: - await image.WriteAsync(Path.Combine(_directoryService.FaviconDirectory, filename), MagickFormat.Avif).ConfigureAwait(false); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } - - + await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName); return filename; } catch (Exception ex) @@ -455,22 +496,11 @@ public async Task DownloadPublisherImageAsync(string publisherName, Enco } } - private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) + private (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) { - var settings = new MagickReadSettings { ColorSpace = ColorSpace.RGB }; - using var sourceImage = new MagickImage(imagePath, settings); - MagickImage im = new MagickImage(imagePath); - // Resize the image to speed up processing - im.Resize(new Percentage(10)); - // Convert image to RGB array - float[] pixels = im.GetPixels().ToArray(); - float mul = 1F / 256F; - var rgbPixels = new List(); - // Convert to list of Vector3 (RGB) - for (int x = 0; x < pixels.Length; x += 3) - rgbPixels.Add(new Vector3(pixels[x] * mul, pixels[x+1] * mul, pixels[x + 2] * mul)); + var rgbPixels = _imageFactory.GetRgbPixelsPercentage(imagePath, 10); // Perform k-means clustering var clusters = KMeansClustering(rgbPixels, 4); @@ -491,62 +521,7 @@ private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) return (null, null); } - /* - private static (Vector3?, Vector3?) GetPrimaryColorSharp(string imagePath) - { - using var image = SixLabors.ImageSharp.Image.Load(imagePath); - - image.Mutate( - x => x - // Scale the image down preserving the aspect ratio. This will speed up quantization. - // We use nearest neighbor as it will be the fastest approach. - .Resize(new ResizeOptions() { Sampler = KnownResamplers.NearestNeighbor, Size = new SixLabors.ImageSharp.Size(100, 0) }) - - // Reduce the color palette to 1 color without dithering. - .Quantize(new OctreeQuantizer(new QuantizerOptions { MaxColors = 4 }))); - - Rgb24 dominantColor = image[0, 0]; - - // This will give you a dominant color in HEX format i.e #5E35B1FF - return (new Vector3(dominantColor.R, dominantColor.G, dominantColor.B), new Vector3(dominantColor.R, dominantColor.G, dominantColor.B)); - } - - - private static Image PreProcessImage(Image image) - { - return image; - // Create a mask for white and black pixels - var whiteMask = image.Colourspace(Enums.Interpretation.Lab)[0] > (WhiteThreshold * 100); - var blackMask = image.Colourspace(Enums.Interpretation.Lab)[0] < (BlackThreshold * 100); - - // Create a replacement color (e.g., medium gray) - var replacementColor = new[] { 240.0, 240.0, 240.0 }; - - // Apply the masks to replace white and black pixels - var processedImage = image.Copy(); - processedImage = processedImage.Ifthenelse(whiteMask, replacementColor); - //processedImage = processedImage.Ifthenelse(blackMask, replacementColor); - - return processedImage; - } - - private static Dictionary GenerateColorHistogram(Image image) - { - var pixels = image.WriteToMemory().ToArray(); - var histogram = new Dictionary(); - - for (var i = 0; i < pixels.Length; i += 3) - { - var color = new Vector3(pixels[i], pixels[i + 1], pixels[i + 2]); - if (!histogram.TryAdd(color, 1)) - { - histogram[color]++; - } - } - - return histogram; - } - */ + private static bool IsColorCloseToWhiteOrBlack(Vector3 color) { var (_, _, lightness) = RgbToHsl(color); @@ -701,7 +676,7 @@ private static double HueToRgb(double p, double q, double t) /// This may use a second most common color or a complementary color. It's up to implemenation to choose what's best /// /// - public static ColorScape CalculateColorScape(string sourceFile) + public ColorScape CalculateColorScape(string sourceFile) { if (!File.Exists(sourceFile)) return new ColorScape() {Primary = null, Secondary = null}; @@ -713,6 +688,30 @@ public static ColorScape CalculateColorScape(string sourceFile) Secondary = colors.Item2 == null ? null : RgbToHex(colors.Item2.Value) }; } + private bool CheckDirectSupport(string filename, List supportedImageFormats) + { + if (supportedImageFormats == null) + return false; + string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); + return supportedImageFormats.Contains(ext); + } + /// + public string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99) + { + if (CheckDirectSupport(filename, supportedImageFormats)) + return filename; + Match m = Regex.Match(Path.GetExtension(filename), Parser.NonUniversalFileImageExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); + if (!m.Success) + return filename; + var sw = Stopwatch.StartNew(); + string destination = Path.ChangeExtension(filename, format.GetExtension().Substring(1)); + + using var sourceImage = _imageFactory.Create(filename); + sourceImage.Save(destination, format, quality); + File.Delete(filename); + _logger.LogDebug("Image converted from '{Extension}' to '{format.GetExtension()}' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(filename), sw.ElapsedMilliseconds); + return destination; + } private static async Task FallbackToKavitaReaderFavicon(string baseUrl) { @@ -773,16 +772,16 @@ private static async Task FallbackToKavitaReaderPublisher(string publish } /// - public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth) + public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth, int quality = 100) { try { - using var thumbnail = MagickImage.FromBase64(encodedImage); + using var thumbnail = _imageFactory.CreateFromBase64(encodedImage); int thumbnailHeight = (int)(thumbnail.Height * ((double)thumbnailWidth / thumbnail.Width)); thumbnail.Thumbnail(thumbnailWidth, thumbnailHeight); fileName += encodeFormat.GetExtension(); - thumbnail.Write(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName)); + thumbnail.Save(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName), encodeFormat, quality); return fileName; } catch (Exception e) @@ -875,8 +874,8 @@ public static string GetPublisherFormat(string publisher, EncodeFormat encodeFor return $"{publisher}{encodeFormat.GetExtension()}"; } - - public static void CreateMergedImage(IList coverImages, CoverImageSize size, string dest) + /// + public void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100) { var (width, height) = size.GetDimensions(); int rows, cols; @@ -898,7 +897,7 @@ public static void CreateMergedImage(IList coverImages, CoverImageSize s } - var image = new MagickImage(MagickColor.FromRgb(0,0,0), width, height); + var image = _imageFactory.Create(width, height); var thumbnailWidth = image.Width / cols; var thumbnailHeight = image.Height / rows; @@ -906,7 +905,7 @@ public static void CreateMergedImage(IList coverImages, CoverImageSize s for (var i = 0; i < coverImages.Count; i++) { if (!File.Exists(coverImages[i])) continue; - var tile = new MagickImage(coverImages[i]); + var tile = _imageFactory.Create(coverImages[i]); tile.Thumbnail(thumbnailWidth, thumbnailHeight); var row = i / cols; @@ -920,16 +919,16 @@ public static void CreateMergedImage(IList coverImages, CoverImageSize s x = (image.Width - thumbnailWidth) / 2; y = thumbnailHeight; } - image.Composite(tile,x,y,CompositeOperator.Over); + image.Composite(tile,x,y); } - image.Write(dest); + image.Save(dest, format, quality); } + /// public void UpdateColorScape(IHasCoverImage entity) { - var colors = CalculateColorScape( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); + var colors = CalculateColorScape(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); entity.PrimaryColor = colors.Primary; entity.SecondaryColor = colors.Secondary; } diff --git a/API/Services/ImageServices/IImage.cs b/API/Services/ImageServices/IImage.cs new file mode 100644 index 0000000000..448dd26452 --- /dev/null +++ b/API/Services/ImageServices/IImage.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using API.Entities.Enums; + +namespace API.Services.ImageServices; + +/// +/// Represents an image with various operations. +/// +public interface IImage : IDisposable +{ + /// + /// Gets the width of the image. + /// + public int Width { get; } + + /// + /// Gets the height of the image. + /// + public int Height { get; } + + /// + /// Creates a new instance of the image that is a copy of the current instance. + /// + /// A new instance of the image. + IImage Clone(); + + /// + /// Resizes the image to the specified width and height. + /// + /// The new width of the image. + /// The new height of the image. + void Resize(int width, int height); + + /// + /// Crops the image to the specified region. + /// + /// The x-coordinate of the top-left corner of the region. + /// The y-coordinate of the top-left corner of the region. + /// The width of the region. + /// The height of the region. + void Crop(int x, int y, int width, int height); + + /// + /// Creates a thumbnail of the image with the specified width and height. + /// + /// The width of the thumbnail. + /// The height of the thumbnail. + void Thumbnail(int width, int height); + + /// + /// Overlays another image onto the current image at the specified position. + /// + /// The image to overlay. + /// The x-coordinate of the top-left corner of the overlay. + /// The y-coordinate of the top-left corner of the overlay. + void Composite(IImage overlay, int x, int y); + + /// + /// Saves the image to the specified file with the specified format and quality. + /// + /// The name of the file to save the image to. + /// The format to save the image in. + /// The quality of the saved image. + void Save(string filename, EncodeFormat format, int quality); + + /// + /// Saves the image to the specified stream with the specified format and quality. + /// + /// The stream to save the image to. + /// The format to save the image in. + /// The quality of the saved image. + void Save(Stream stream, EncodeFormat format, int quality); + + /// + /// Asynchronously saves the image to the specified file with the specified format and quality. + /// + /// The name of the file to save the image to. + /// The format to save the image in. + /// The quality of the saved image. + /// The cancellation token. + /// A task representing the asynchronous save operation. + Task SaveAsync(string filename, EncodeFormat format, int quality, CancellationToken token = default); + + /// + /// Asynchronously saves the image to the specified stream with the specified format and quality. + /// + /// The stream to save the image to. + /// The format to save the image in. + /// The quality of the saved image. + /// The cancellation token. + /// A task representing the asynchronous save operation. + Task SaveAsync(Stream stream, EncodeFormat format, int quality, CancellationToken token = default); + + /// + /// Gets the RGBA image data as an array of floats. + /// + /// An array of floats representing the RGBA image data. + float[] GetRGBAImageData(); +} diff --git a/API/Services/ImageServices/IImageFactory.cs b/API/Services/ImageServices/IImageFactory.cs new file mode 100644 index 0000000000..f556cd19ec --- /dev/null +++ b/API/Services/ImageServices/IImageFactory.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; +using System.Numerics; + +namespace API.Services.ImageServices; + +/// +/// Represents a factory for creating images. +/// +public interface IImageFactory +{ + /// + /// Creates an image from the specified file. + /// + /// The path to the image file. + /// The created image. + IImage Create(string filename); + + /// + /// Creates an image from the specified stream. + /// + /// The stream containing the image data. + /// The created image. + IImage Create(Stream stream); + + /// + /// Creates a blank image with the specified dimensions and color. + /// + /// The width of the image. + /// The height of the image. + /// The red component of the color (default is 0). + /// The green component of the color (default is 0). + /// The blue component of the color (default is 0). + /// The created image. + IImage Create(int width, int height, byte red = 0, byte green = 0, byte blue = 0); + + /// + /// Creates an image from the specified base64 string. + /// + /// The base64 string representing the image data. + /// The created image. + IImage CreateFromBase64(string base64); + + /// + /// Creates an image from the specified BGRA byte array (Output from a pdf page) + /// + /// The BGRA byte array representing the image data. + /// The width of the image. + /// The height of the image. + /// The created image. + IImage CreateFromBGRAByteArray(byte[] bgraByteArray, int width, int height); + + /// + /// Gets the dimensions (width and height) of the specified image file. + /// + /// The path to the image file. + /// The dimensions of the image file, or null if the dimensions cannot be determined. + (int Width, int Height)? GetDimensions(string filename); + + /// + /// Gets the RGB pixels of the specified image file that is resized to a certain percentage. + /// + /// The path to the image file. + /// The resizing percentage. + /// A list of RGB pixels. + List GetRgbPixelsPercentage(string filename, float percent); +} diff --git a/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs b/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs new file mode 100644 index 0000000000..81eaea5b4b --- /dev/null +++ b/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs @@ -0,0 +1,227 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using API.Entities.Enums; +using ImageMagick; + +namespace API.Services.ImageServices.ImageMagick; + +/// +/// Represents an image using ImageMagick library. +/// +public class ImageMagickImage : IImage +{ + private MagickImage _image; + + /// + public int Width => _image?.Width ?? 0; + + /// + public int Height => _image?.Height ?? 0; + + /// + /// Creates an instance of from a base64 string. + /// + /// The base64 string representing the image. + /// An instance of . + public static IImage CreateFromBase64(string base64) + { + ImageMagickImage m = new ImageMagickImage(); + m._image = (MagickImage)MagickImage.FromBase64(base64); + return m; + } + + /// + /// Creates an instance of from a BGRA byte array. + /// + /// The BGRA byte array. + /// The width of the image. + /// The height of the image. + /// An instance of . + public static IImage CreateFromBGRAByteArray(byte[] bgraByteArray, int width, int height) + { + //Convert to RGBA float array (Image Magick 16 uses float array with values from 0-65535) + var floats = new float[bgraByteArray.Length]; + for (var i = 0; i < bgraByteArray.Length; i += 4) + { + floats[i] = bgraByteArray[i + 2] << 8; + floats[i + 1] = bgraByteArray[i + 1] << 8; + floats[i + 2] = bgraByteArray[i] << 8; + floats[i + 3] = bgraByteArray[i + 3] << 8; + } + ImageMagickImage m = new ImageMagickImage(); + m._image = new MagickImage(MagickColor.FromRgba(0, 0, 0, 0), width, height); + using var pixels = m._image.GetPixels(); + pixels.SetArea(0, 0, width, height, floats); + return m; + } + + internal ImageMagickImage() + { + } + + /// + /// Creates an instance of from a file. + /// + /// The path to the file. + public ImageMagickImage(string filename) + { + _image = new MagickImage(filename); + } + + /// + /// Creates an instance of from a stream. + /// + /// The stream containing the image data. + public ImageMagickImage(Stream stream) + { + _image = new MagickImage(stream); + } + + /// + /// Creates an instance of from an existing . + /// + /// The existing . + public ImageMagickImage(MagickImage image) + { + _image = (MagickImage)image.Clone(); + } + + /// + /// Creates an instance of with the specified width, height, and color. + /// + /// The width of the image. + /// The height of the image. + /// The red component of the color. + /// The green component of the color. + /// The blue component of the color. + public ImageMagickImage(int width, int height, byte red = 0, byte green = 0, byte blue = 0) + { + _image = new MagickImage(MagickColor.FromRgb(red, green, blue), width, height); + } + + /// + public IImage Clone() + { + return new ImageMagickImage(_image); + } + + /// + public void Resize(int width, int height) + { + _image.Resize(width, height); + } + + /// + public void Crop(int x, int y, int width, int height) + { + _image.Crop(new MagickGeometry(x, y, width, height)); + } + + /// + public void Thumbnail(int width, int height) + { + _image.Thumbnail(new MagickGeometry(width, height) { IgnoreAspectRatio = true }); + } + + /// + public void Composite(IImage overlay, int x, int y) + { + ImageMagickImage tile = overlay as ImageMagickImage; + if (tile == null) return; + _image.Composite(tile._image, x, y, CompositeOperator.Over); + } + + /// + public void Save(string filename, EncodeFormat format, int quality) + { + _image.Quality = quality; + _image.Write(filename, MagickFormatFromEncodeFormat(format)); + } + + /// + public void Save(Stream stream, EncodeFormat format, int quality) + { + _image.Quality = quality; + _image.Write(stream, MagickFormatFromEncodeFormat(format)); + } + + /// + public Task SaveAsync(string filename, EncodeFormat format, int quality, CancellationToken token = default) + { + _image.Quality = quality; + return _image.WriteAsync(filename, MagickFormatFromEncodeFormat(format), token); + } + + /// + public Task SaveAsync(Stream stream, EncodeFormat format, int quality, CancellationToken token = default) + { + _image.Quality = quality; + return _image.WriteAsync(stream, MagickFormatFromEncodeFormat(format), token); + } + + /// + public float[] GetRGBAImageData() + { + float[] data = null; + float scale = 1.0f / 256; + if (_image.ChannelCount == 4) + { + data = _image.GetPixels().GetValues(); + for (int x = 0; x < data.Length; x++) + { + data[x] *= scale; + } + } + else if (_image.ChannelCount == 3) + { + float[] temp = _image.GetPixels().GetValues(); + data = new float[Width * Height * 4]; + int oi = 0; + int ii = 0; + for (int y = 0; y < Height * Width; y++) + { + + data[oi++] = temp[ii++] * scale; + data[oi++] = temp[ii++] * scale; + data[oi++] = temp[ii++] * scale; + data[oi++] = 255F; + } + } + else if (_image.ChannelCount == 1) + { + float[] temp = _image.GetPixels().GetValues(); + data = new float[Width * Height * 4]; + int oi = 0; + int ii = 0; + for (int y = 0; y < Height * Width; y++) + { + data[oi++] = temp[ii++] * scale; + data[oi++] = temp[oi - 1]; + data[oi++] = temp[oi - 1]; + data[oi++] = 255F; + } + } + + return data; + } + + private static MagickFormat MagickFormatFromEncodeFormat(EncodeFormat format) + { + return format switch + { + EncodeFormat.PNG => MagickFormat.Png32, + EncodeFormat.WEBP => MagickFormat.WebP, + EncodeFormat.AVIF => MagickFormat.Avif, + EncodeFormat.JPEG => MagickFormat.Jpeg, + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null) + }; + } + + /// + public void Dispose() + { + _image?.Dispose(); + } +} diff --git a/API/Services/ImageServices/ImageMagick/ImageMagickImageFactory.cs b/API/Services/ImageServices/ImageMagick/ImageMagickImageFactory.cs new file mode 100644 index 0000000000..1e7d8a556f --- /dev/null +++ b/API/Services/ImageServices/ImageMagick/ImageMagickImageFactory.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using ImageMagick; +using Org.BouncyCastle.Ocsp; + +namespace API.Services.ImageServices.ImageMagick; + +/// +/// Represents an image factory that uses ImageMagick library. +/// +public class ImageMagickImageFactory : IImageFactory +{ + /// + public IImage Create(string filename) + { + return new ImageMagickImage(filename); + } + + /// + public IImage CreateFromBase64(string base64) + { + return ImageMagickImage.CreateFromBase64(base64); + } + + /// + public IImage CreateFromBGRAByteArray(byte[] bgraByteArray, int width, int height) + { + return ImageMagickImage.CreateFromBGRAByteArray(bgraByteArray, width, height); + } + + /// + public IImage Create(Stream stream) + { + return new ImageMagickImage(stream); + } + + /// + public IImage Create(int width, int height, byte red = 0, byte green = 0, byte blue = 0) + { + return new ImageMagickImage(width, height, red, green, blue); + } + + /// + public (int Width, int Height)? GetDimensions(string filename) + { + try + { + MagickImageInfo info = new MagickImageInfo(filename); + return (info.Width, info.Height); + } + catch (Exception e) + { + } + return null; + } + + /// + public List GetRgbPixelsPercentage(string filename, float percent) + { + var settings = new MagickReadSettings { ColorSpace = ColorSpace.RGB }; + using var im = new MagickImage(filename, settings); + + // Resize the image to speed up processing + im.Resize(new Percentage(percent)); + // Convert image to RGB array + float[] pixels = im.GetPixels().ToArray(); + if (pixels == null) + return new List(); + float mul = 1F / 256F; + var rgbPixels = new List(); + // Convert to list of Vector3 (RGB) + + for (int x = 0; x < pixels.Length; x += im.ChannelCount) + rgbPixels.Add(new Vector3(pixels[x] * mul, pixels[x + 1] * mul, pixels[x + 2] * mul)); + return rgbPixels; + } +} diff --git a/API/Helpers/SmartCropHelper.cs b/API/Services/ImageServices/SmartCrop.cs similarity index 83% rename from API/Helpers/SmartCropHelper.cs rename to API/Services/ImageServices/SmartCrop.cs index fdeb50f2f0..b568fcff45 100644 --- a/API/Helpers/SmartCropHelper.cs +++ b/API/Services/ImageServices/SmartCrop.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; -using ImageMagick; - - -namespace API.Helpers +namespace API.Services.ImageServices { /** * C# port of smartcrop.js @@ -33,22 +29,16 @@ namespace API.Helpers * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - public static class SmartCropHelper + public static class SmartCrop { - public static MagickGeometry SmartCrop(MagickImage inputImage, int width, int height) - { - SmartCropOptions options = new SmartCropOptions - { - Width = width, - Height = height - }; - CropResult result = SmartCrop(inputImage, options); - return new MagickGeometry(result.TopCrop.X, result.TopCrop.Y, result.TopCrop.Width, result.TopCrop.Height) { FillArea = false }; - } - - - public static CropResult SmartCrop(MagickImage inputImage, SmartCropOptions options = null) + /// + /// Performs content aware smart cropping on the input image using the specified options. + /// + /// The input image to crop. + /// The options for smart cropping. + /// The crop result containing the generated crops and the top crop. + public static CropResult Crop(IImage inputImage, SmartCropOptions options = null) { if (options.Aspect != 0) { @@ -59,7 +49,7 @@ public static CropResult SmartCrop(MagickImage inputImage, SmartCropOptions opti var prescale = 1.0f; // Open the image - var image = (MagickImage)inputImage.Clone(); + var image = inputImage.Clone(); if (options.Width != 0 && options.Height != 0) { scale = Math.Min(image.Width / options.Width, image.Height / options.Height); @@ -115,7 +105,7 @@ private static CropResult Analyse(SmartCropOptions options, ImageData input) var scoreOutput = DownSample(output, options.ScoreDownSample); var topScore = double.NegativeInfinity; - Crop topCrop = null; + CropItem topCrop = null; result.Crops = GenerateCrops(options, input.Width, input.Height); foreach (var crop in result.Crops) @@ -133,10 +123,10 @@ private static CropResult Analyse(SmartCropOptions options, ImageData input) } private static float Thirds(double x) { - x = (((x - 1.0 / 3.0 + 1.0) % 2.0) * 0.5 - 0.5) * 16.0; + x = ((x - 1.0 / 3.0 + 1.0) % 2.0 * 0.5 - 0.5) * 16.0; return (float)Math.Max(1.0 - x * x, 0.0); } - private static float Importance(SmartCropOptions options, Crop crop, double x, double y) + private static float Importance(SmartCropOptions options, CropItem crop, double x, double y) { if (crop.X > x || x >= crop.X + crop.Width || crop.Y > y || y >= crop.Y + crop.Height) { @@ -161,7 +151,7 @@ private static float Importance(SmartCropOptions options, Crop crop, double x, d return (float)(s + d); } - private static ScoreResult Score(SmartCropOptions options, ImageData output, Crop crop) + private static ScoreResult Score(SmartCropOptions options, ImageData output, CropItem crop) { @@ -184,10 +174,10 @@ private static ScoreResult Score(SmartCropOptions options, ImageData output, Cro var i = Importance(options, crop, x, y); var detail = od[p + 1] / 255.0; - rskin += (od[p] / 255.0) * (detail + options.SkinBias) * i; + rskin += od[p] / 255.0 * (detail + options.SkinBias) * i; rdetail += detail * i; - rsaturation += (od[p + 2] / 255.0) * (detail + options.SaturationBias) * i; - rboost += (od[p + 3] / 255.0) * i; + rsaturation += od[p + 2] / 255.0 * (detail + options.SaturationBias) * i; + rboost += od[p + 3] / 255.0 * i; } } @@ -200,7 +190,7 @@ private static ScoreResult Score(SmartCropOptions options, ImageData output, Cro Total = (float)((rdetail * options.DetailWeight + rskin * options.SkinWeight + rsaturation * options.SaturationWeight + rboost * options.BoostWeight) / - (double)(crop.Width * crop.Height)) + (crop.Width * crop.Height)) }; } private static void EdgeDetect(ImageData input, ImageData output) @@ -208,7 +198,7 @@ private static void EdgeDetect(ImageData input, ImageData output) // Get the pixel collection of both input and output images float[] inputPixels = input.Data; float[] outputPixels = output.Data; - + int width = input.Width; int height = input.Height; @@ -237,12 +227,12 @@ private static void EdgeDetect(ImageData input, ImageData output) } } } - private static List GenerateCrops(SmartCropOptions options, int width, int height) + private static List GenerateCrops(SmartCropOptions options, int width, int height) { - var results = new List(); + var results = new List(); var minDimension = Math.Min(width, height); var cropWidth = options.CropWidth != 0 ? options.CropWidth : minDimension; - var cropHeight = options.CropHeight!=0 ? options.CropHeight : minDimension; + var cropHeight = options.CropHeight != 0 ? options.CropHeight : minDimension; for (var scale = options.MaxScale; scale >= options.MinScale; scale -= options.ScaleStep) { @@ -250,7 +240,7 @@ private static List GenerateCrops(SmartCropOptions options, int width, int { for (var x = 0; x + cropWidth * scale <= width; x += options.Step) { - results.Add(new Crop + results.Add(new CropItem { X = x, Y = y, @@ -265,13 +255,13 @@ private static List GenerateCrops(SmartCropOptions options, int width, int } private static void ApplyBoosts(SmartCropOptions options, ImageData output) { - if (options.Boosts.Count==0) return; + if (options.Boosts.Count == 0) return; var od = output.Data; for (int i = 0; i < output.Width; i += 4) { od[i + 3] = 0; } - foreach(Boost boost in options.Boosts) + foreach (Boost boost in options.Boosts) { ApplyBoost(boost, options, output); } @@ -299,7 +289,7 @@ private static ImageData DownSample(ImageData input, int factor) { for (var u = 0; u < factor; u++) { - var j = ((y * factor + v) * iwidth + (x * factor + u)) * 4; + var j = ((y * factor + v) * iwidth + x * factor + u) * 4; r += idata[j]; g += idata[j + 1]; b += idata[j + 2]; @@ -322,9 +312,9 @@ private static void ApplyBoost(Boost boost, SmartCropOptions options, ImageData { var od = output.Data; var w = output.Width; - var x0 = (int)(boost.X); + var x0 = (int)boost.X; var x1 = (int)(boost.X + boost.Width); - var y0 = (int)(boost.Y); + var y0 = (int)boost.Y; var y1 = (int)(boost.Y + boost.Height); var weight = boost.Weight * 255; for (var y = y0; y < y1; y++) @@ -379,7 +369,7 @@ private static float Saturation(float r, float g, float b) return l > 0.5f ? d / (2.0f - maximum - minimum) : d / (maximum + minimum); } - private static void SkinDetect(SmartCropOptions options, ImageData i, ImageData o) + private static void SkinDetect(SmartCropOptions options, ImageData i, ImageData o) { float[] id = i.Data; float[] od = o.Data; @@ -410,7 +400,7 @@ private static float Sample(float[] data, int p) { return CIE(data[p], data[p + 1], data[p + 2]); } - private static float CIE(float r, float g, float b) + private static float CIE(float r, float g, float b) { return 0.5126f * b + 0.7152f * g + 0.0722f * r; } @@ -423,6 +413,9 @@ private static float SkinColor(SmartCropOptions options, float r, float g, float double d = Math.Sqrt(rd * rd + gd * gd + bd * bd); return (float)(1.0 - d); } + /// + /// Represents the options for content aware smart cropping. + /// public class SmartCropOptions { public int Width { get; set; } = 0; @@ -457,8 +450,8 @@ public class SmartCropOptions } public class CropResult { - public List Crops { get; set; } - public Crop TopCrop { get; set; } + public List Crops { get; set; } + public CropItem TopCrop { get; set; } } public class ScoreResult { @@ -468,13 +461,12 @@ public class ScoreResult public float Boost { get; set; } public float Total { get; set; } } - public class Crop + public class CropItem { public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } - public ScoreResult Score { get; set; } } public class Boost @@ -498,48 +490,11 @@ public ImageData(int width, int height) Data = new float[width * height * 4]; } - public ImageData(MagickImage image) + public ImageData(IImage image) { Width = image.Width; Height = image.Height; - float scale = 1.0f / 256; - if (image.ChannelCount == 4) - { - Data = image.GetPixels().GetValues(); - for (int x = 0; x < Data.Length; x++) - { - Data[x] *= scale; - } - } - else if (image.ChannelCount == 3) - { - float[] temp = image.GetPixels().GetValues(); - Data = new float[Width * Height * 4]; - int oi = 0; - int ii = 0; - for(int y = 0; y < Height*Width; y++) - { - - Data[oi++] = temp[ii++] * scale; - Data[oi++] = temp[ii++] * scale; - Data[oi++] = temp[ii++] * scale; - Data[oi++] = 255F; - } - } - else if (image.ChannelCount == 1) - { - float[] temp = image.GetPixels().GetValues(); - Data = new float[Width * Height * 4]; - int oi = 0; - int ii = 0; - for (int y = 0; y < Height * Width; y++) - { - Data[oi++] = temp[ii++] * scale; - Data[oi++] = temp[oi-1]; - Data[oi++] = temp[oi-1]; - Data[oi++] = 255F; - } - } + Data = image.GetRGBAImageData(); } } } diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index 36048ae3d7..aa63a274ee 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -823,7 +823,7 @@ public async Task GenerateReadingListCoverImage(int readingListId) destFile += settings.EncodeMediaAs.GetExtension(); if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; - ImageService.CreateMergedImage( + _imageService.CreateMergedImage( covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), settings.CoverImageSize, destFile); diff --git a/docker-build.sh b/docker-build.sh index 072676a783..326a17f5ce 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -100,4 +100,4 @@ Package "linux-arm64" cd "$dir" #Builds Docker images -docker buildx build -t kizaing/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push +#docker buildx build -t kizaing/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push From 361c46499d9f08b0bcfcc9e9c176c287fe1795e8 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 3 Oct 2024 21:12:28 -0300 Subject: [PATCH 23/37] BOM Fixes --- API.Benchmark/ArchiveServiceBenchmark.cs | 2 +- API.Tests/Services/ArchiveServiceTests.cs | 2 +- API.Tests/Services/BookServiceTests.cs | 2 +- API.Tests/Services/CacheServiceTests.cs | 2 +- API.Tests/Services/ImageServiceTests.cs | 2 +- API/API.csproj | 2 +- API/Controllers/ImageController.cs | 2 +- API/Entities/Enums/EncodeFormat.cs | 2 +- API/Extensions/ApplicationServiceExtensions.cs | 2 +- API/Extensions/EncodeFormatExtensions.cs | 2 +- API/Extensions/HttpExtensions.cs | 2 +- API/Services/ArchiveService.cs | 2 +- API/Services/BookService.cs | 2 +- API/Services/CacheService.cs | 2 +- API/Services/ImageService.cs | 2 +- API/Services/ReadingListService.cs | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index e975bd00f4..3667de4146 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using API.Entities.Enums; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 7f0a9b7b7b..497effad15 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index 48245e6392..a07dcd49cb 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.IO.Abstractions; using API.Services; using API.Services.ImageServices; diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 2bc18f8b60..15df966bf1 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index 3f63228e6c..2497f02be9 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using System.IO; using System.IO.Abstractions; using System.Linq; diff --git a/API/API.csproj b/API/API.csproj index 169ddfab90..3729b96899 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -1,4 +1,4 @@ - + Default diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index 5a03798793..32e4a86f1d 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Threading.Tasks; diff --git a/API/Entities/Enums/EncodeFormat.cs b/API/Entities/Enums/EncodeFormat.cs index 189b127458..f42ba4586f 100644 --- a/API/Entities/Enums/EncodeFormat.cs +++ b/API/Entities/Enums/EncodeFormat.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; namespace API.Entities.Enums; diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index cbcad82063..c2c44ef23e 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,4 +1,4 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using API.Constants; using API.Data; using API.Helpers; diff --git a/API/Extensions/EncodeFormatExtensions.cs b/API/Extensions/EncodeFormatExtensions.cs index 301c69fa5f..21b57ef43e 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/API/Extensions/EncodeFormatExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using API.Entities.Enums; namespace API.Extensions; diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 715fd0b1b2..32d82f9ffb 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 46ea4c3018..078ae0500a 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 27096f9cb1..4df3f422ff 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 6c0fe17a9f..f123ea9f38 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index d20f8584f0..194053b9ea 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index aa63a274ee..5452424852 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; From d6056b80d1eeac87af310885800a6549e4f70a6a Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 3 Oct 2024 21:16:57 -0300 Subject: [PATCH 24/37] Update docker-build.sh Enable back docker build/push --- docker-build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-build.sh b/docker-build.sh index 326a17f5ce..072676a783 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -100,4 +100,4 @@ Package "linux-arm64" cd "$dir" #Builds Docker images -#docker buildx build -t kizaing/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push +docker buildx build -t kizaing/kavita:nightly --platform linux/amd64,linux/arm/v7,linux/arm64 . --push From 92f131a2a36e1930f384164759e61f08d6ccd543 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 4 Oct 2024 15:33:25 -0300 Subject: [PATCH 25/37] Fixed Benchmarks --- API.Benchmark/API.Benchmark.csproj | 5 +++ API.Benchmark/ArchiveServiceBenchmark.cs | 41 ++++------------------- API.Benchmark/Data/comic-normal.jpg | Bin 0 -> 457527 bytes 3 files changed, 12 insertions(+), 34 deletions(-) create mode 100644 API.Benchmark/Data/comic-normal.jpg diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index 2dcf08f323..38c09b577f 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -26,5 +26,10 @@ Always + + + Always + + diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index 3667de4146..31f3088e82 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using API.Entities.Enums; @@ -25,8 +25,8 @@ public class ArchiveServiceBenchmark private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly IImageFactory _imageFactory; - private const string SourceImage = "C:/Users/josep/Pictures/obey_by_grrsa-d6llkaa_colored_by_me.png"; - + private const string SourceImage = "Data/comic-normal.jpg"; + public ArchiveServiceBenchmark() { @@ -61,9 +61,9 @@ public void TestGetComicInfo_outside_root() } [Benchmark] - public void ImageSharp_ExtractImage_PNG() + public void ImageMagick_ExtractImage_PNG() { - var outputDirectory = "C:/Users/josep/Pictures/imagesharp/"; + var outputDirectory = "Data/ImageMagick"; _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); @@ -75,9 +75,9 @@ public void ImageSharp_ExtractImage_PNG() } [Benchmark] - public void ImageSharp_ExtractImage_WebP() + public void ImageMagick_ExtractImage_WebP() { - var outputDirectory = "C:/Users/josep/Pictures/imagesharp/"; + var outputDirectory = "Data/ImageMagick"; _directoryService.ExistOrCreate(outputDirectory); using var stream = new FileStream(SourceImage, FileMode.Open); @@ -88,33 +88,6 @@ public void ImageSharp_ExtractImage_WebP() thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp"), EncodeFormat.PNG, 100); } - [Benchmark] - public void NetVips_ExtractImage_PNG() - { - var outputDirectory = "C:/Users/josep/Pictures/netvips/"; - _directoryService.ExistOrCreate(outputDirectory); - - using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail = _imageFactory.Create(stream); - int width = 320; - int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); - thumbnail.Thumbnail(width, height); - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.png"), EncodeFormat.PNG, 100); - } - - [Benchmark] - public void NetVips_ExtractImage_WebP() - { - var outputDirectory = "C:/Users/josep/Pictures/netvips/"; - _directoryService.ExistOrCreate(outputDirectory); - - using var stream = new FileStream(SourceImage, FileMode.Open); - using var thumbnail = _imageFactory.Create(stream); - int width = 320; - int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); - thumbnail.Thumbnail(width, height); - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.webp"), EncodeFormat.WEBP, 100); - } // Benchmark to test default GetNumberOfPages from archive // vs a new method where I try to open the archive and return said stream diff --git a/API.Benchmark/Data/comic-normal.jpg b/API.Benchmark/Data/comic-normal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..91a8f9b8e8f7f55c91e9eabf2990c794b94bfd0f GIT binary patch literal 457527 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⪙WQ98J-#(ZR%bzjA0xNwJ*uP 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
  • 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%0D;% zqCn{j-Wc;SuXy;sZ{b4eJAgB5ZKgA=iU2sX*MZWPpq-P149Dqj&jkU^n!oAr)ILui2>xiP#ieY1)a%ZYQqxakCX?KQ8z>a+K@q8z=6wGL zcflhi?s{~lr~=@yYHdw4(r7rg!6ew&Y-On8LEjG*uVX0mmRGbtGZ~19ik28hD+tAL zZ)06ZJw*4+h`cw+Yf<=Ohx{Cym1&_=mjCbQmHEOpYL2J+knpt z-G8{Ah1Q21e_FDS<|o=*PjJnZ{eA}luU&RQpULw@8+}!LK%DngcNu+GqyEnHG`Wz; zmnoX0&?H7fV`3$bz*~PydnS~oD`#*>c%DLgTmqQcim~skGufTm%xf!w_#aFgyLe^q zcztDZ#o4%Z?5_Q%+u~EVZ4XZy0;MdmW|3{5OXYm{NT$W~OldHc{$~Yaf)a6=*>^XR zRbziK%QvCa#!x!zIboJYY*&pFC6 zqqVlT_bZtkFW1{;$}{E#+y8)TK75E|Hl&W-eWPv}s@pgnLwYjL4Pzopk0xGym2%#O zt)6(WIkhxI;u$lzpyBH2)EZMS0Y2h36^nH^x#{@e6G5OEoYR2T-zPry2P^=_)G!v_ z)kuRW-_;y7W=g-GqTg&-_}@mo7J2x|yo<$)$nR}Fubt@GoOI=SpiH|K(o`fUDU77y zHN~6t8=Li0Ow!N`Pi=^alKaP%f`rA@R>KIT8+F^k8msktGT;6=YN?~kOM7x5pQp<; zgYNf~%r}nQvABeK_^s{H2@>?-EJ35883EEZzIDGw0u(;_m%rA)RVsC{SQB9_zQtJ< zmst1?BZ1bTlq)zFtJx_-ugvaaOVO9D{&%S5%xUH2aXN~U}mXs zUwbpH%J6$&T0y_0mk}OdR(ZmU0aFG-Qft~#cyU$1&qZ=w*?i_KP`D zzb{kZ<3=%|e)5Rg=-vEu6yU*|if8Bd<Fe1?kVc3*DmIO~8)2ejocFQt@4L z(hfLUNJ(|PpiGNq@ar1T%-$~yzO6pXv?zUwl>iV;fuepHHC-2rS(wX%x%Riuyz3#1 z{5(GxO*)X3Mwf3!?3;6uNnH!*3=C<{cAA_a^zdhwwpStA^x7 zs?f*=Gjk;V7=6vd&>MFwFM_>Bm0Og&r*LSYh5CS`eWv2=FROML13nPrCWne8!FSQ? zb(UrjbYDt0Ptq>B>sM4Z?`oxe; z=jV#{NPkhU4YB)1kq$xSEcn1LpRuZ#Ywhw{qbAb7-rkOa2mZ8#1{aX6BO_9&t^EAJ zTJcyM4@&AJ$2t$!Xj(r~W2C9BvQq7xfw!LR*aX`LJ2@#H3H3yKE@ivyPvb;!2lKT% zL=Gd;25x-KMFqo@?i{JhyUfu@PCd8WDrBknhicG19QgSI;H(vC4i61=A&rz zKKcY;1&~y$J%ma1KUsA3*TNH2AYfi1}It- zs;ao%r2A&_NRjrl6y|c&K{wPKYsaiy42W_E432bX00h-u- zAT+>_XYuQq5$+omQvOpTT=@l4H`=(noQYk5D2>%f)5ovbojo`Jb@1eW5Q+?9-;t4f z_4B(*2B*+cXXXdn~J0F=NoOFSo3&pvr#5EbD1J}pzv?(mnoaqehA;!l%Jfhc4 zY_2AkOp+l^%AE7=Rw4gc z5Fvx2qxK&W_rt6g=OZXu@Ys?SGDwRG}})xky0i&Qa|l%GCn zCR5;7_S3TeqmY!v`=|+lyJKocBp1%;zKr0xd5XJ?05ZIzD-lOTu~`20yz`ih8%Z78 zI~~0YrXDxyDn3J^08aLt2bA9kg9qPES}>SrY58kUYyK0demlVf>IPd>#I z<$M#it0-9Qd!61N-l&ow>hc67b&0M+FVArYC7nko+X#`hOpi>PzWT}d!*i=UU-WFN zgR%C)luUyM04I&A~zgP_}Cq8R&6-M@bQ?nGBSXLI3eaeXt zgnhEPdS{=YkGp#@Dd14#A))F8k@ELVns+>|MKRlChhXE*K>e5;WQitq!9C0Ta;Yv6 z_&np|BR=+Fip$^&YNMOL_oQH2OP4(`ni=73Z%f zUX+8tL3%CKO+nyB!N4s=Q}gJFROWC|sm{B&(0s2_4n$$<)70*A5g17ffU=D>^Hq>R_U0*O^bvc=By+qbobd*VFz{44STTy@YRDvmsrmt{fj3!w*&)yRjdP*aT5l! zAy*?FPWKCvOLyPj;r z=t8Cayw@MA@G&SFr{sCvXW>nC15zU;fZmii!rmFDL*dz z#e%>TY+X_?u1_b_r+8k=Al8c{1tetquv;A_eP3qTKr*D>B1yuY6ciGVbmy=u{swTH+OuQp2RMD|h^Xdp+;;ATCgenZ?&w0&lwZE-gnRUsA4TdO!~gB5;(x7fmF@~v=T-^v zs&VJa%NU|oiZ37+QQOD-2hG0tL&nLv_R1;GoUb;2*8j*FxuL1Jy1Fcu1&4a|W2owB zpRd%fNLf#EWAUyVHA;$(QdFRY0d=b*S#v~OXxVAZ7Bl2eOul}o!5(Qy0u04WHyf&- zz*N*({-4s)E&}xAQ<0<{fp4*A9VG*%e@)Q}-CmAVAo%Styw_1;o(})I*5BLS85ZGW z9`slu%$Z-zD+&O%7*JW|0Civo^9pKSyS>`0oIcXFd}8o`&9y`?Uxp0GJ6U$n4fmG? zpgR{T4S5A_ojJE3R#EX`*CV^>ii>;A1NzQbnN53L+F#ekVT!NaJXOdsZKTHhR6oaN zA&0T8giq%YM<(j^5M@VJ-7tjPh7a$8v!9*7Dnaa7{&;UM+qKSxXo+KvH`pv}j0h!k zggdu%Xpr`So`??$4zW8UabBvRbp8Rcw^@&svPb`#4lIWo8!*%O@#9zOE_lTS z1sAOC@04EED;{&L&jd%zbvy(3hdqQZd^mlalKhiSIkC0h_*;-wlNN+F@fs;gizf)t z$eRIt+Wd0Zue7_Ic|I#j*IZ8h0RiaWDWHPZty*Xu+C19BCHrO@VYVu?k{-24XX!<- z&@I6*r*0x=$)`^c%J|%X0h=$+q_L0a)#>8f4~_4tDzx0F6Hm!AYfx>wk`JEyxDMfb zcYo^FEkQ!*~LvY z0md7~i7KChMZ2veP3C`-yJqaWJgI1-Q_X3UKhSi>AA(vwj}5L-W%iP*Dt+*3R#M%e zz|@I%_*HDEfG+>hJ32;;|DQo9oC+3>CUW@!U3g3f;~f|A{H_C`_8vo~m$&OOE}413 z%hWA+jYD$IoZ56wA$uY`7|dYijZ}`wi+fnrsMbA zQ8HqYquld#2IpKHe{|v}xpHE)*AI)C&IaH9<7FhGOR2Y2B7NG9o$yVRKhNfy?_k90 zl0<9^qX_Q1MZ22#6X`8@rR$ojQHSeN7zSzJGQXG0zYJsc_eabOPOOp-q9U_Ao}VWf zmQwY${~A?^{H{_Ml5X}eLY9_rc#{L%qh+fA8oshD#%pT3N)j zLe0^AaWX{5>>cNG#C?xJXz$s1+DTM|e|oc6KCRr>FRzH9sQxw)HA0u&2(6p$bK)~P zt)lW<`X7&HXgIZ80ghI+X?5`0cw|~l zTTh)xpR*Vdx9}Eh#$N{{j4{fD#@MB*r#*^z|0u%|BEKdSAx4?XR)D!?WYhYp*c$t9 zG-`3x{%P{iFxOV4IrZ7M7W9qv+m8c{ZnH9yI_5~7tIjk|D;-?s3vTO3(p=&af2Kxe zB4G6Y)nqu_JvSRwkSdhe@T)Syo$Kk6{O@YhK&OO?ZbN@$2#jb-4V0FGv7ZnG4P4Dz z{Az%c#8dkJQD=LE;Z|bB;BbgN@K(4Jz_Uz8WyQ@%WT51B!Hh)QX*QI{-x{7{5XzAo z^@EsBZf%LFEa9qW6iq^Z;sxyggO>1%4)l!VT1*d~D>yr~StfjAyD?_IClQS_)dmbH z2Pj=T@;$D!%DwW4#C8TZoOa)rB zXLI`mg3szYjxy^=fnGRO-lkznEPxj|H+GI?R{ZTScI&R}LJ1{Ri&{c26{;luVo}?Z zpI8f`2wgQj;gFF#d1>aQKXOaak@vHhb;-S|BEvh_1!i74|Fjpe(fW&J*q$t+dC-P? zG#g1r{^?$=P0OH6Lv-!FwuO041Au2_eJpFB<)O@I9O;7HE8s z=Tal;xeVwSV4yzw=!F39@?CM!N>6lE06hWs2f`?3e|4I5yZH5_C5bGHnW#Ixia$8G zGfE+oZ>|~$W43B?s#}}Cel21%H?D*oQ5iK~%*>P_1WFyj z!F*QEmnu;mnWbC+Xt9A1niA>J(^o5U1<0)rl zKvOUtjk9}AnHaFKX1n=R)-Fz`2Tcq#>DAm`q^Org%I$q#3w{Z?os{9%|2kdmCyyj$ zO!X!O6S#^5&%)d_@c0vHld+D>KK%)8Z$>Z9cI-nvwN zj>G?)F0El4b$D^@qMzXZ*3946KfEmfkx&ttMP1^gw;->f*quEES>~igzt#|0GAcP6 z=QXrj@+ft4{M9EY{&Q&EK;?&3yrPbJtDakEvcJNN5g#J+wjE#3ph*bi_D@oRDbUE1fF+pn?Y;(Gm1mo zmW_aCG6B7ebj^~!d*X}&*X9*P=!TN7 z{RZQm%hR1%8y~+VLaD<(ew}Yu`ug~}!uqR+dEdna-fSklVqEIVgU(x^Iqy;w?zxKg z{GA4{Tg-ueX3J;W%NJ*2X_2HY0kNLd_q1b%kFp62#xWKV z8r>1MMSt2pXVK+yI{50NdiInlNU31$9M^jzl*0!}sV$d6TnS#>nu;}j4y zzxAJ-Qd^AvG|2)8dt=J#D~H?5q5ZwS3nu_4XK=JeZKRzTIXx#oWVx3Vw?el@-N)rn zGxP%ByLY%Gy{Qg6A{-ob=r?x<5l8pHpBfpq?^iY&7ZHCF(%0zDL@9mjCLGbbkLg$3 z*H-Nx5U=(Hqb7GFZTWw!e})naKI&`8=xe7(>Sa3axhQLFL`z#dZcXM1DbNWj_xA#> zM+y~y;C7TasOfV5P~3B{JbsU!I7cOy$ya%J6gp^kad5wa8GtL*bif7-v*x*gU>My# zWMj^K{r6sIo_DALG-SFF7y_w&(WHOX>J8;7TCv5biCa%=UL+CHu@+bh*#8IKnmMC_ ziuNf%Dd@JMKiYE6_RbfJl1geMB6&HsTgzyA=M>Q{K6-DV;p93hz~KQhTxz{}qcHOt zP{-^?z=*`3U2vTK+&fyUxtMSV^bz0Me*G`+Qe?zqNg)xxzdqJ2_QH;gX*$`ZeZ3l|7gnI$`!Fl+*No-a z6NN(kg*gqO@>&gv2+sb{K!sa%9gP~F2JKYWF8A587nOBwBlfSj*#5V$&OZyIMe9{RH&wl6xp6ACC|eE#jx{n@>z@Yx z%q@~4cwM~WIS{QN_!9vrd=7P$;Y(BtW=p$dik1&esdWu#vje_U{u3;u&?O+fv9;=E z4)eSviZ3!DdLPW|Ym_SAWr!x;aw%6VIxJcY_j6viUy^r*cS)p4x;A7RxUw64G{g3E~ocjRInVYslyLKiMR{b@i^C(--8e4TzZlBz~?L}fc-pRht$b)KIk&s7WIR6 z6QP+l{8{3h->BCXqexeZJIF8_(uF-ipN~+F?yh8#ekn~z6|EEXWIv8#XO@eZQ2FiB zvUje6l_Q;L>kqwJ{g`CuBQwxx6AuaPJJCG_^weN&)M&>@1viMQ>rs{S#s*W=rAqJJ zW+nM{(z(%7f*4Gk;1p=@FdcJnJ<>A8TP)Qw}4vvTKKpWMpVDuEe60_R_NkS#` z48p7dFLhkEj^hb#Os@lIVX$`>ha`yi=bI{Ee&%g^gGJ_V9Bj7~Fr&xsM}E1``o+ch zLbGFziIqUVwe3{{NUw3 zTM2S=tN1+Mpu!!36%t)VqvZ$8wbOiLr(`e9qpB-VoVSpXWw^CNuG*6(jEEf!I~pZH z42K&x8F?qyM%pQj$!qZon|6{8ssOD5P;F_jjhYbu4`pu|)@GwF3sZv%g%&C9P~2S# z6pBM}x8m+nLV?t<;O_2L+_lgYOK_Lc1`>)CNw7fR{hoKNEo+^#_qnd`C%+PsC%4SZ zJ%g%ot2OnxoXYqa8TX$KlEM>(B53SCnckPQ8-MA-q!K;aPyzUl1eUzr zNl=rW-VMRGw+lb82uZ2?sI87NkBI)rQ?1m-Bm|LV5ghfEf9ZPpr#za{Em7@F7~&HpOMZ&CGaMYUr>TeS#u#H((J}g%_@rtThgzUnVu84KPVXvftIXDn}!zuCy?&)Z0q1+~xc;5xT{pLBTeBl1tDnd43C z5_SK~|6;s3q8q=NsUrBobrRLT%`3-zvZR%thXp-cRga@BEo*8gxxe%|P+D*!I|&^Y)YIM>kezrhfFOX|5v zfzfZQvZ=TNbpOHuf~VE6grP*}nd7I!f^oX1{(l7POA*vyq`Fw zx9=#|O!4VKY(??oJ|`h`cAP|Jc#bw_CZC_dV6Bry%6!j#DdyQ7X+Y0bxmr8nX}AWN zwF8_DcsmBov1Qd?;*gJ)HGDv%GiCv7{Y{;9S%I11iMS&S@&?A8O5uCtVe6%*pV{Qb zY4wr_st?T1N)x)6aofbpcj$$MC>kLe1~B&7_%8M1ShAugH4HgK*2AolQp-8X;J~?9 z+|f{u0rnrc3h4!L``lKE2_=D}+BsoaM;m!%NESz#Tr?URXMqwnb1Je3)=$WP&Bk=9 zf+TAGI%Pa0WX@1nkbaptCqk*^YFEur;**+)7`KJdfvTuXDVqBHa47U|*^+xpi!AR? zJR|)uhZx0Imt`lPd2g}v)m2%^aUJ;zeSWctIR#>x4nvxe9@-d^gGX^7a?G{yUwBcu%fID%85laDbD{im38U7yj7xFc z5b;HdmTOxRL}-|#2pge8r3yrd z3aQexct=j&_@UR&x31S*xO*4VpUW_1LVCN6`f^iu`^Pp~bMC9!-!uox4AT$qTqWs_ zBo~iT5ysxS>(7Yz^R;z}WVY*j+1mSRF0~~*K}ABywiRx4o;U+n*xz;MydA%~=v}(k zvNiEa*mB?$1-ovJj$@F`c<(6%auIoxhXtd7S3Vto(5LLmH?MA-ZIRA z8*@8h&PZSI4DP!88m!I67JJn3;VhYoz`zIdd!2?Wm+E@s=TFeZ^YOg zHbnX=;Zx;;rUyf`3TV=>7q_wCFkkNCabjTgOrY^M{@Pe*c4L*?k?89E#BUs)!L4?) zx!rk@DiBb`*rAYEL0EoniNC8+BsSw4ZMzu9E{iqG8iI&NXnm6+-OuFp875wcsfb~F zRbqKy;3(f_^}B+?V1&^q&`Avz$EEVxV(Ls?jT0Z`xVc4@CzGnHM*Yfq{E~r^_AO=2 z{vpg73rhf~T2ZW`!PE(uySKdTofGR5*uq(^U9!AUDWu_{IAP8BP{In-${GOuzheJj z-(n^0DxuqtADl|BR@pf_slG`qbuA^kzT#(yLSKZDXt}wMIZC(vGI(-^k zPbGT??a^oeMxE!0Xukkm>0)F*z2Rx8shUx(qcY?m2NGeSNBosz6Kmjt`VhktabQA1 zzC|vgRFLrDG!biFv4Uz%CKKezpKsn#TPmJJ1wCs_)=j-{q1LQB!Jo53_PFRH!9cq% z#uefQPpky2?OtEh9Mp4y28Z>*Mqf1p|GXA#_GQ#DAl)Djj?W8h;!vM|y$g;|Qx|&m zMfF(|nFMVf(9`D6Hs^tuF4TY&V$H`mhGn7e$$tw7g~VX9upZc|+U9Ew`TkVuSqMLF9@6s=bK_Hh$wwQd zYxC^hO}V*ZSR@B9_o**oK7ZlOHCKX18UamFmRm4i5fZK(%5L=GPyL@#=kkb`$|{zS zhGWX=JO8?Nn(L0tk~`&V&AOV!scvvt35S3g3a)HYrpzMI2g9I=3Wk$9Wb$L zY}W`1XNgnzurIU}Num_Ln{o=z=jf_!Qr;1}iEB#cm^^0qmFAF^j?*oSE!SB?C_wT* zg>Vf?=47qt&u8YY89wx1bX+DXAjgA;DQs^NKL^(Mqrte9;4X$xsleBoT&jEkx`VJ1V zDBDPz9ve18pnD+olb4FXxP!&OL8%r%!T?1)Mb|s^d#d=IDznmuPNYiMYha1prAXxa znUv*}cPA+wI=wnaBl}@cC>m=)(_;dO@PP z88IP-ifu2gd4MWsN}Sxk+01ZR4^&FY<|%Nntejs=<)w91e+oiy>jlG11#xw~Hg)!m zWE1?9;u&pLn-2W%LAzk5`725r#yy@{#{VqZwe17vQS`wBke16?8aag!aXG_+4XpQ$ z;Bq&)f@`*6dbki z6Vx|1Q9xt@a2PNEs;d??jL@sAfh$Y}h%8o{j)G9}eEl&4s{0-MfhNuD>bTt93BJ68 z^gee_VCV8(pncfz(=dx9t%CWV-0Qz#LjSMU^4}Vuf7y**?xw7puQV^K?5?>#eP?hk zJH-Cv!Fml7W3W=XcqvFY7eGDDyv%l1Galj?0jLsOWUqEy6Rz z<5;>ij6O|V)>^3n zr+Wc>mhK~t-gY#GrZ>!G)y5h7=bjxkgizD|t+PDjy7grCi^v>PQibdTk!}}OLB`lT ztm?xf>t|cCLDe8&_n{~RDUQo!QRnAvso7@8D?XC9$ykV`h^AIj<68STVty7UJLztC zu#rn|1XLRLCR|PS=BRk!45*c*HK@Y^Hq)H(Qb4ei^Nd>Ap>R{H%eFqQ(MSu)FQ8Vs z&cUNVA{)zftu+nEIr=%AW|ZpFZ}$#F8woVDc#OR_%zM8c+>Uwo)3^|8XK6k{hW^6o zbk=?wB~{QOwUwDzGhkz<4fqt`y;c#f8z{9GK(e;u#h6?;wmrAhHG{gWVQJ5TX)&jkECq}dt zCR93gZDFb&>+w|4S1*a1lc>DEXB*xr&J_#bBbZUScF1CKXm`h%5AHAA>z6t6bCt8SvR7Un5R z(@EGoCVy|etCfr^q0qMKU)W(lx+-$z*pcG-$?r4wdOr`4NSX1Jk*4vEPz3z*RM>|b zZEOJVhpx>d^TJmE;~3wsaMPn$&$jb+=2$<=o9Ma1cUFCncSFL8Tx7^Hv1+@C%G2`K zWVc>UIkv5*7(jKXdp3-leIHZ)olSWZlcMb64F&wb^@Qz&_6n)AQvL4$vhj&q=ndRf zdcz|1>?*oi{sZ38O2sj|K!Ui}d-sc+31f>=K(|G~(SuMNjfOe4cGTE?;hGG|AmB6> zVus0n!{yAtQDt_Xa<2CEBh&@+>13Ky{B4EnnaJdTBt=*J4=R&9-{+H#hjCj0!@QoG ztq+Mi@fAYW;KEx!LRvdiDs5}05_I&cuAmp~tG}+yQ9NriSIsafmpQNiSfp5-J{uff z_he+A<(&($*hBGUZEchYl_J5IH6}6rA(#uybDFQcE1Tux0exTiRO*S6+~Qw&A2uO9 z<0HY*5P#NlSZ2D-Ir|nk^QP*Z2X7e3SM#k^+N4*|$i?Y4BSpwVQu8K?Gd;z%s&c!n zXHXjV}>mg922bVh?e;&m_k7$wAeqr8WH$A#Fmy=@9S= za5SGH?k5i(-k#hds=^Y0DwbT&7tY^`ABo@?jp;M>p2b=`oUfV1`WW5YD!AB(-qies z_wBX&3B!+sgf+yjY{stKR{f2}HO-ApBu=f_DQAz5RZ47lj+e(3xyg{bGo#rI3i zw{_ME9g7F|>~nFf8E{p4CBkYEl_1fVyIGKxzg0bVrvWUNWNoj{2*b>~7PxrleUcLO zivRYLm4Y|u*P(&q$ek+uJMZp&umB!gerK<`G8lzkdGvU0x^ig>u7>TUfh?==6-s`d z8gpp#3zxrKx+7*ro@(ZqM>6DcJ3L{buOA( zJ*{Lm%0>(>ltEQ6(7?B8$Hx^}=IoCfjmK76KkkJ2EdJ4%f4r3WF1V8RhF$FLno1{p zCg8jS=?VP@+iSF?*L?f`S7&Me*NdnB7EJz!FsjyklA3C6|EBeR?b*-lpn4yiANSo0 zt+!p*R(ujm+Z3>{plN8@Qg2k)Zg9%}_03IZ)gMAy5(I78c}G`Bu0nTaxy#zHZB>7o z$hEZ5qPx3jOhMJo>>89D3g_N=bwIvVfqkf9TVD(o0keUbd+L!IaO<(YK&kjflB)KW zT2BYt0Uu^j!hT|w@p7(6pyYbt;&Ao2=_!6uIcaRni4wAPvb=#NCJ3Z7ATGD@5w53t z5$prCmcc@1z7Lu7w9X*KpFOqEpzHabmdxhM;-}TeT4D?EQ=`D_SJ@Ygd7X!B>xv)s zIjJtHTVHYT@c0K&1;pN45;C$`5Dt;@^E9+1WOLY@dD-I=O%!Q(ay zZHDw_VB~Im7*x>W$Z?>3Xk(}?g;ve9PFYBNpM$k59z0V#GgBBk2ixoXg(t>^H7EDb zCe?-X{g?qWmErnvO`kAidsFM`9)CQoW;meao~Go+b0}@rQx*R8geU>1)iw4`O@YK$ z86Wj$K`h4dswE`O0OLi+pvyHtmr24AqGBka;%myk2)I&N?+<=20FI*#bPZ5yd>r?; zbnx{wf(ZR!D_1*-!U|taAjEr7&zA0NCQR_d_V(@t6=lBNy)~{ZFi}7Onw>CS>G{$u zA$mkM7FB-lLyEqBq5{AAJl~1NQY#I`$-$d9$E|mC63`~(a?i=2I_IRVuu17~!hMr) zo$3do%SMorugXK@@6K_XfQ1MGI5fI0vDz~W#fd#Jv*7KzJnnu#6DfumpM7s%e|b@} zc+kJS^213z)EFkFy*cqfuYil<3sfFH0sep;nQ`N(skHB>}?Jw5rdtJEn} ze-cQunjp2yJ9CG|2 zW!2a!z@nGCd}-9XAx0;BE6(+$mu_Vw;yT|_&`5iW0ejw_9x{!D^IP>T@*CFIF?J?P zAY$JjO_xMXcopf~X+utaM!xXRKZA~_+h+J&De82L zvYXtJvgQ_~S8*hLt;}L9=1u5V>@B--i*~Q`HNfnw^MS^E1+)cof7F!63Yxe#p0%6h1>w0U{E5@%KMz}lM@ozx2uCDmLF8lWYi zG8l)lRgm>7H-1ssR?gUrSu@UQfAqk!S3)UIFZ!OHd<2Bv`-m!nv;&wh|0LfBdj`ue zeYd;S4-Ye*dG*P(S3cI(Xz!-85@kn;NfJD)obe449otFSW|*kw`<$<=d(^QdU?b{t z;PqkoSS1J7`l_dJsY5PEf;Dnd+&c%9Ew=lB>YllmV0+ec@@fGaMa9N?-C6Qr?d?g_ z>F~BwE-Qwop|#~UW!=vY1zE4+T)I8c|1-y^17`6o?#A`bXet*mIj_2g zsPEH=fre{461Prx{_cJ&*F_kpi5yaUzGTYb<9a8h*I`Lg!U`X|pOIY4n)+b|a{Gvo zWi$u*4|_S#L`8+Q^G24K+1h=0#ja7Eowvb$Q<;5uR_L4%8kYDn{H^Drc`uD%4&A@A z(-!i~kv@*HJJ3@*N%E|XfT zziJ6wf=w-ac`8{I9tzxjI`t{j^BavNspFqa^m`!RJIIx@+#i*^eR>dfRC9ZmaqQh5Wb;gQf1|2d9O z9`j1={LkUys2?`Tugu!jVsK#^>f9U0-9D?EmBoTUFy3%(qhxrxeXqUlOuSf@CfD?T z%zkoK<5Ct~(tbI2JhZ17G|W)>71SN5RA&MRCBksaKrfZV@FT4=$}qyamzLMz9r(Ir zP}Ykm!=Le->{gJq{!xjV`#8YUJU0+ifVxTXsRg<1dfaRI5yN`UNfzD_abei46Sq{~ z9%6{R43AxLSX(eAJ1tBq4r3MA`dn4=IMX12vYLTh#W`DPF;i69lKC5cS5LNd&?f;M zE2I1G8<$m{?*`k}R$!#jDR6MgO0JnoPF?!Sfpf3(yuI2Tm5~ar?Xqti0Y;e=)syJ}e+oJX|on+y5-1XS% zcUK^ebewtV&R02+9#p!HD)haG2sz+&f9EFyoL^F`#QUeHl6FXwa%zvlfuG3dTqs)| zF(3_T29FVsx8asBU-GuLy3C)?(Z3>X^1H9cp0RHz>HF(jh~gjH`l*faek`;eo-mdt zWXUil8~kzNyUPl4liI+OFt!>nr9Ds?gh<4-K~10HGH^G{1?VOf9q?ubh1Z#-jE_Ov z9$Fjo-PTAOe!IN5$lXqXvwwd?wt&{~K>+pvcF{+$D+8l$XkR7c@sW`LYNS&T1hwJ^ zuwE=y1N#Y81Ncz$hP`RaOmd?zI={{6a0!c6|6x3DC1>&leq|B1YFh`gyYV@<>cTB9 z_VkOR?#;KAIXF1N!vl^_Y1m4jx-46iC4aXrSKq|Tn7a_6-_^EIzmy0=Q;OsB+K^{In7 zLCay31L@j7WiK5SV}u<}?#<;IAdiT6#>wU!t0Y^8DGZa%N&$YT0hg`M>o;?W8mPYhoA}0VVSiy0sxA{)f|7kRyzdcAWbfaURtRnllrEdmL8@2lIRPz@J+7w> z$rXa(;w?G{<2L8>_?+?!Y@hTWy#j0If;zG$CX0&5AbqU4>^U*?YL0~l*i}K2K+V570lAJ zX)c+~zTZ?0&cfF%CvSgWkC{^p=?;)nqz-6l&VMA z`EC#D zy@AJD%gyl93=-NblwF%N0AU8##mBgNiBfI0wOhs}Hy9EOR`jXf1~H+;<#MUD!b!)1 zMGq=y_h+bhc~>N|mT+35seX=E7VkH(&Fo*Or^FQfQqr6W4S6UHvsm(!hxUG9pG3Mx-s%T=~zoPL2`J*_`eMjKwl8K8boZ@n@kGQ>)DqDr&VTqgsI?w)?a10k zm-L65F6pS_k?@DpeEC5qAM9y-q(219U0e7C~WnThQf#H!J~&ARO0H0N@=*!u=d?GShUs% ztMS2JO6-HJMV_Ptp=ADS zl)>d>YRhnMQ+pzmg(s;BMJ8!z?BFP)`db=NFOkmvQ5j;$%}i2TVS9h+6QmYu4CNl! z|r=UC4kfYKsr7ZCiD z@7Ag6z7S-wP3QlJjXQ1oQ21-A>VnX!;EGCKO%D=WC@ z8fx~fS;GR?LBqyU&?}CPJX<1zeK@Tf6*%VzwSg2{B0sc7WXffN-|lW10=K;50?{B^ z4q_Qhf9A{>#l0@iFwh&of{g~Zi~A-llf?3;Kcqi}eU$p3@;pWGd8=ds^Mbls+TX4K z2A(N3yVUpg&C=6k(%T!vIm*Mwq{JajQ&mvYfQ@Y@l-7y%y~)f|Q|3gU0JqU7o1ZE? zhODw77=647qdrD|2GflGD;~q=$!91hAvNyiwykv5f-IA@j_zk$YRFB?V?#jxomutT1KI`2X2|Cn!$lWsRR6#{v`}t+@tXjg2(`Ar z605|KaMO#r;5nWt-BhqWb+>nx9maLd;OCTuI)u}QeqSeZm%Zssy%VKhrx1K6D--d{ zKKZ2=#iAF9@R^%Fng$h;KS*bMEp*M;q!PH9d(OooGLNWx9#phZOZoh3T zELDHeK<}ZLtQ!cPQ9CMk7)l9G$@xhPM^LL#`U|V8%=nR5tbZYhzcE^YgRyzY)pr5> zob&D@x^v-KmR%QIDVejr8V3xp7G10wwn(?r6Ino^x#|j29p$!>cZSe`^X)im)vE;MLoK zn&%MdR6$q`^HYV|JGW(39+n!@E1_Fwg|*9;rN#B$B!t~i$x0HH#o>!qhP}G6+SRnu z;^J7!E>Oys`>IOyW>RoI0Z$&_;rp}V?$D>&G zJ4PI*>n^-5N|Af7!bO(mfTtRgdGSMWtB2meX>E%cktc3Ir?e>?Q4Vy7|~ZJZ;cY)u%KrJ=bTl1J}=tB)nP)CSt}-_B3bL?0_en- z=I$uojJ1pgHQqR@JUG9&jFWHK#tmMszqFOJZ^ADwaJ3u00}Dwn`_Ty& z3+U%n`wiMq=xBY}ZMkfOSR+Qsgt1-QG2GYKkrPFH@zR_*HK+5Oc%i73#mDPFuErv& zf8EC-7{y|{^teQb6ST!ik%GJUeUAtCJvWso=B;NxoeIv9T>K^gmQXD>f0d18csGTi zYuvQPBuyDB&GOB^4+`L@GAd&Fsz))ru_|WklQCIhbvXvdyBq1DGq<*wxO zYs$?sU6sWvtiG*wt6Z7`WKNcsFogmO%Qd3@sZ`mOQ&era!O9AZW>s?(q6dqur!7g6MsJG`nbW+EO}nU_?Yi@%S(iu~SiQx?Hz4|A6jK62wor!+1u1P~D~j^}GrcuUOQLjM7<7`!-*>nrm-%W!7>_)^*ExAGI0ySF)d z^8dnWBt3eRxIbDbNL-S&$7y6REut#&KMpaU=}S4NliR|5($v&p?WZW%-xhzdN}e&acEyEwp+j80@@1M20=dw0WL5z$ z?FiYCjxvsc`9n@0&$)Q<@S3?$tp;w!ayv{f%%sAa1_V5k?iKkL@tD@WWp`1K@0I(wTfL|Dy>8BR)` zbl>5P^Gh1QvH@zX8|JsmfJt5{NJMmEj`OuSUnjnvQRcl!^DQTp{B-`Yc2p_ltYb06 zFz!Ybw1L?elQo!Q$2CKKo!oa4vJ^3zeodkWc|beAb#+U_n4o78H2|>VlK95=(T#8XCT9EPjKAqgX@iGoY#cMp9t91Ry2sUHoIrY(2`p!D z4PG@bc7cH}|AqdTB5n=q*NQXKt#p*B+JlilIrn6$79GHB^EY5gX?V2GW< zJoPd^L7jts*{rl3{9`19`+^Xbz>aUR0i@C8uqUH;Msu&RQJ`)MIx99TxPAx7KO343 zs57JDOEB8Y<}1t0Mh?3xM#N!~k<4F+((>`$=2@G@k5&}(MOHI~{& z+5TM)*WaGX95=+Ht_fTU`1eHjIV|d#ZfFTs1BKt_%=ZMR+thI>G&m9y+UOy*_9}&a zIe<|1l`-?r;=NH{!VU48x7ZLB5O+0BDBf;o?_%p$+l1!h$iadvGr2PE?4VzX_Eszb z3}s9fP+nIF@)p z?4OJtn-oJ{H$AY~$XdCz=ueQ$fBxm`%o%=_9z7`x5_coARq)ypVi0;Ikk~M`f5<$? zi|F~hz?o#Pzdw|C4bmlpZ!;9MaRzL;Wvx24WsgwUsZl4O(Xy#VI_=-x2MH;2n>Cf3Ez=Nb}?EF~Cha?S}(y zGVl33ONIt|hql_8En^5$z<{zD%#(CfA@lJ)RhxAf=jqc8`^z}azD(G3}x@$aJy61qPsW23=`j9qhiG$XsZt39$;deTNX$xRwX5;gm&^P zMdrlxQgz(=nwY=~_>fQZJ+;Am*@O;EPaRmNMQA2r<{#b$Q$HqoUd^!tdwG2AbVCp_ ziD!4yT*bOA78v;>Kg_+>=u#^_%M%jKJHr*e#G$(sD&cb~t5HdFKmOEJ-+fU!qR~ha zMLE%2X$lIUO0`raEK^c_nH8jD(Vgd6x%}i1b@oVF!v8Ah5AWZh@=Cf(gm+iH$vC)w zYW#bZ`0t_tvd%X!2hD0{1)kO|)BNVuY|niZGY*6SN4)7a%3EOLLqQ6YLnAnz7qH*B zyFRLPt>Ky@K>d6t>~Yyl(Gn<_;O&2*{Z#0XlQ>nhe%iQL(kvZ~T z0#6SKQG}mbr?md6S&m{|LEw@1k6C@_|Kf?U`z8KkYsb&$dxAjTu3)q~D)xX{oZ-V5 z3w%7Yqrh+TP^RRuNlJrY75U++=ykCi=!mF7>!dMvuG14J@Tf`bm^KV7g=LmwbY0Zx zON=J-9Di6lQ_804!g9f+v~Flxb=+)Q)6Xuq%`St)vX@%@HX|{E+l0Vb*aihTS@>`N z9vKN6VR+=l$Uy!)7X3OPZrUJUw_$V#{p0|K41TEZ7je#X{0&-Qk(wGr>C92-jW zFH2N2z*i0N$rd56){R_5;6})LekQ~Rhpc7G++!UTb|qVgp(Js08WTIF0>z#U7G#guhEsoD zM4Kx!N6s@R72^3Q;c>-vZB z=r-1;`b!n^Lj2;_^=v4e+%I1~`j<+X-`5+SF})A-aNZ~t0oO9m7Xf3_yxi=Q_1Hsg zAP7RF#d2+Sn1@dT^*DI$tK@2*?T9E@4r9dshD5V(eJ=JI|HD|nKVX0xYIDb2JA8|L z4$k}&|1VOZ|G5-(ydR2rShe(N&9+jDZ+_o>vn{boFrMW-LgLfWRABKet5#7|n7?xJ zjrATARKAr*_RUA3H8~R-StZO_;!Sy;=TMK@WrU@y!-n0ZO6N1*w)qO?8gK4@d$E6T zln+a)o_``ERU^2F2zA^$F;1`B>g)G|2Jz}Jkc1FK7hjB_^jV<&2p~D81tu1`(ndNw=Es*Oo zAi_DBs0c+p2oDmU|5?9Zx!!l(Pdl0T>o#8n)n-!GIY9-I&-~v$yMG%rF${3Zo4V@X zm*HVgxnHSFMDoQSGkqIxL4-^@xeo-6666N3&bCyMU3Y$nlRswhJ0a{R3@ZJ#+vGif z-N*7zh`YV5Q5!0KqjFMjJuK3-bG#*MF44iF`+&$TH?k$n1oXxJq}=fWVMEq>T`DW5 zf)K8m**b2V-ewpG_;#V+IMNrM8W9~OO)h<<&q;KF4gq67;?3DH{)N{SiBm-}xP;oR z?fCa=L!%wD;Qtf7{&xecioDi=TK_ed5cc-QmI7p>a!ePB5pZ9*0BlUCYkt7KWzV5x zVoW*ljsDd%`M*Ag-bW*0x*2Bv0dH=}l8=5;BmUCQ)f2yBIZdo07>@Try_vA zcFT#SpooVUjS?AGVf*E9AkKH-S1Ns(xW(jmx-h-;9q&-jQ@1mHgq*UyLk6Kmh%O?d zGaXg6+D*pm`j7l7#e4bIwQC~%ux}_d%V+(PB!pPb&a^*FGkom()9llunU!PJfa1w>K9c&kX_rnX~E8 z#86$VgVXq|S-=}1^awiagUivS&R1e|F>wcBcVYmYzTHBn#ygV5T)vc`)4~&bK+k9E zCxG@YzM#N_sq?%E54c}XA2FiW=!{uJ@gy}}cI|t@9 zT*Jz-1mnYF`6ja`ffZF=w+SjR1<`-u{fZuM(tx~b{ib6N(*I9!{r~nv{Zrkc+Ouby zosr(ktzxCYRyz2pe?S%IJ3`GAy@gd7oylt9HnP(rO;}Tf_4MlouGo1$m>zqS9^TjA zHd}J)xn(j7O|Z_0XMS#xscy?WG3sySHtopDJN6JPs3YR~>2eqKXd`zUngZ(p|DTL3hUK42<*o^za z0COy1lpO52#-S)?)<3ChwSGt>|HSnO+Xn=;cd{f)+~iHZtII`Z>@K~PO+o{NbONU- zUqOj^SXQAph}@^x9S$;CPn+ zbWg=kgbP$D9@RMZ&&QSxO_3M?lsI@G5V!8Qu_u-KYnf%Mk^^EI?Zj1hk6xl0yagWF zcyVLrCcZ+P!Sfh;mJQ`woQu2rN>Ke-piY>G!v)B;_RJbP2%Z>Ora0m7bWAhPOR`hF|2d`nK8}>1ZYVu@kVCo~$ z&%YV0zXwlEx!njZz?W|x?SvxE+M4rXW2-q9--AUzv+tQv;hUF?|N39#aD5x#Xz}Al z4{e-Xjv>LOk4>CilC=tyAyhb?lCoD$4>P`?vfE}nR$A#Nsc#LC8=oBQB`GL9Dd1m0 z*S&Cir@E(Zlso5Nd39_ch<4g~Z(E^_-e5EIeL*7?9_($`z8t3U?Pig!!on~C)U1K~ zJbMxjA2Nb8dnV&7cB;1f{ z@eC6bx1q-Ol(a%^C{M6njPw=I$=N@{<%vXB8$IWqqpk4JWb?=Y2rhhFH*9+OX_>?6`Br;Ah$5tiz4}+7Lw;uE9P+!EYpV(6x(Bo>Fyw`u?_;VmT^@zI zHI6@&X1@u&kmZeVYof4=6+DD%99B8YCW@Te<6Tw9t6Vse#pFG~{lUTKLe=bDPg$?A z^zoC4Ay?JoyP9F3xaUn7TjYPYERU@VkY>M3A0guE5!y}rw0?h+r8edgJu}R-d`>Gp zA)DJ~k!KGoc97AH+9xug(RhGyA0^1}%7YUWZtgIDI+n~}C*ptKFY){$NrtST2gMZF z0g`oGd~7e7@O&l(>HPQaCpwCEy)>&sdf;dlRt_~0&lXLp0&N@5ZHf}; zcio+h|50=6=xirl{!p{{(hu!q`E0Q*h4cvhaO3$B5-IeI!B9M&0A76t|LU8YaJlJtL%>7yTG-|Yu%k&CRACJIndre!(w#7y7z^dqE(LE>;qn6} z1ze133CiQW9IEGh5)gQ}N%DyryrZMP@XSxTaw8#O+rM2zMPbhu{}St>;%TeGWuf)ocMc*!LJp^&g<`$kRt3xqovDeJ?$| zS2H?thUj3c+@Z@;xp%F+yo5`{^%>Y-KfOoO$*+r+sTL}r58dy$b05Qx+vLmOiF=Eb z9u--QtD+$}4m~}kICSrl`ZT7n6lHWQg0@uF76egaPTuC!S(0Yl>(({d7>Hna^e`yI ziaVXw*7g_L&u2Gk1-z5efmBOWBgI#os;ZQXQj&h-mAnt&=@}sF(aUpGU$dWsowtXG zo8ouVIg$rAN@bFg8?MKX`W%COzLD@`sO0ceFyE?-oDNgL6{Fjen6#@_Sk3QPcX?wc}z{{t0`}4h%0J?pl7x%GA}b`$EKU< ztE=yk1>LcLkG6fC-?=tsf%S58_yOmw{P13;*2e^U-c>GtSWR!yDdZJgh)-uAL@Zut zdWuQ0N%0kT)qOU0RWem!`Z-hylN|m$+X$7F_?QYh9ho82I4eyeBiat0a#-=%^=XZq zw(_!Tj2dzy)Z{P{(UajQv^9-azaaUKGl+ihZPJ9 zc5ATRx^;N{y5{wR=PEUmm;%3bNX_zEYu>lFj~2uppVQ{D4ZftKOtLLdc3uG{%Y#3i zh7R|qKfaS&shI8yEPe2lanB|tg5*QUdEHvsj}n>bgE*Eap*6W-pkr8pxXms3yZC>r zpZ{f|!DLcAhZa1b#G4uQ$VRPNj^8?z> zT~){1X?uGLSpi0c%G6nQc0zV`5?R)rtfX51t>`VB_||y4@t79DW$Kw!KJo)M`>#et zkG{~v^NsqjA*rM?DR!mcVO+ZubWv@>&yoCCpQ&hwRHtgd`0s1kf>fh0{b>Scoiq)Y z9#j&vgt9+KIIZ-KgD*{3K4d{Gpl-sr8y&-c1K~7ww#Y zWqC6HeyjE-QJ!EGMK=HXOa36Xof1HXHc9S(xxH4kqqxhy#s*s7zP4lkIX=h*k(POJ z+&Q}5Gr{i_Kg45^9|>o7)&i*1CR*ALInI(9R}h)f2_x|@Zh`H9mbC|LuZY&WxRQclcjmxjifs_^97`q4Nv(wnrAjXH*AX=j4=Rr^u=t za`dnno(*W3n<1IUQJr79Kz`4Nb8Vb34N^NBKmO2*-AzvxjGhn8q-?Ju;ts+a;ZE-G zrutpAtA8n1GsuagqK!;D#LQUT#be{k(37g4)IZ zl7aTHbdO7V%8v(chnBTm$C)H1Bt1JFEqTI@xn|7bU~*iK8j#_*RaELm;)Y2#t*_DA z#2ir*;(zXL{hvf{+jgUujSh`pSyt9wggTt~wqqzV zi&(VO_g@Ly#uO2N;^}o2O@gk$PrV2&XvXls?s=_;!oRdF{byaAM0@uk!wEko{UbKF zcAsnpYk>o0?_3dA`Fb)+JZooHp}(tAqS=FTovwpmCe+{Fftj9^5)^VAgMiSW&ko}5 zxh^)dLJmahGp|k=lly6^`}WuE?LB+$LzUg3b*FTNd99M)t{tMcifq^lIktfcFaGPX`ER2)Cl?6P zNUgBGyT2v&_gDj#hW(I$Ak5%f4J&Il#|KP>uQ+&RyH5+v5X~T4)aY}<7&8ftd+?pY zS`KNRdqfNg6ci$c;%3Ubo7flqxIak^FaqKCzAb_IxBc!q;~4>%m< z5I5X+{ac;b)Gl66$k_b_eoK~(5N``v+dfta42RP&ondF`MC@IOi9;&ZX`s$GW#NA>(YVVr54d=L5Q+aX(X1v z$&}iI*%dM08cl7?E#zM&Ym6_d(jH4MVCN$n|63{gZ+br+g6rZq$e+cN=ih24?;3CY zs|){YGyG408nEE2);m=f>j31awPgSe51;|Grxo1_&YnZqHtch@nhH+hKpJ`9U+`7#S%*h;3JQ7IuLFqDV0NEK`OYtPKsyK`5(+v0A3+DTW(Po6Tp^bSq=s zS4$)aw5nMf+!KvFCXUIO64NFDdw!G>r$jgE(J=WA=%|tH^zqX4Cu9UvhRC7hYqe69 zPoU+CYekuqAA4qu=fEv$T6Z;}Zm$MM#hWuFEwo~U z0C#kejP9}uM%vkdkH*o0BfuDL z!+GA7dT!#w>YHW};kF(A*UZ!`PEy;-LKwCR$;eiPG)Y=$e2IvaIJ}^Q2-SHB(|mHT z^g8GJ9ajIF{#>R2{nL4~=iatiM=>F@mC6x9Lra){S#I^_Rkr z%8*_!$^NTGCJYH5Xm?YyT#7H)C77@oTK*;2RV1&wJ2kF-7t1M#txD$QwKn%u|3w^|xFn2)J+OTE9d);WKT&4@H+4kfptt7gFiHY9VNpHwxP(u=^%8VY^^u6WY#5ezaloG6;oj}2&bwj!o z`YU=Q`w^76CH#bNBp+maXQR07Z`%Zk-!s0=<@9ryf8gLa&I!4H2NF|w6D*PRW`ooA z%R;;*oLkzJS0_fzUi>tbz++wVkwg_xkr;_BQ^`=cXHttP-F<#rsweAohs=49nZNh`WwK1aMzF{-fdnzb4Lxj^NiCJgu=VRzqN{~D3sy^dhlJn-|ynH zxDfOGh)DLD>|f$P)_`o&>Fh~cv?B3d^@Io`Sb-UPFW^#D9g;(sGJE9Q%~bgOCe2~f zNXtf}>FW#B^FhPIzsiK+P$9aW7VKRfscYJ2L}$5}LTkVIlmCZmWQ*Or)v_cz;lw9i){h zgmLZHt|I?8fkI!NzxzOQW)F%u2qt~ik?@H5T%6HgLEwLvf(jtuDyw36BIIauIcjcQwFb9J*jV|3V7CXi;{k)=%jpw58^{*Pw&Um*U}3H2Y0H3*;sU9a}9 zhAXIel2lfrtZF>|<1YSPEp+=-+6183z3TLGJFroAE}^Y{rH2PEp-HbAzywpFqvM#~ zH#$CpaU}Az!y&aX@8X4evtyt!*p#*j=E~Y$_85C`qlfREcZ{(a66vc@zEC_wr-4`F z>|jaAZkYh-h%-)=>1f+Ycir?WF}8TY?~0KoSjJtt=Mr#a`X}|Xt1otBvha<^1DpHS z_S}6gj0)}@cHP-l_kRHo$IN67EWf4ssYpX7^$CdY6@mc#IYc=R?pc+Q#hLMU`6IOH zE&~#M#t_RZ8C6J%EGoG(Y{Zkknc3}5sjJGB-8*u*h@7K~JiePYX;v&$YhPO7M@LK= zCYwg2bvmFo6VBnymRqphFG?l0W0rsj zEks35txykh0L<8QfJ<3+b;*2wX!j+ zJG6iF?T6Ap1TaHMkLwMyK#kTzT{#vwaf?P*zU_Q^!dJVNvl85&-C)FnTNoINb6YN%3AzVQIMC}!*5?uQ7)1S3E!j&hHN&Ae zd6E0Fdi1WvRnnXGU)hRKi|k$vcj18RS=B_n5~J5h#P06EX@`tV^WB(8VCc_v{yVSe z{8RO=BEN=W!QGtqs)qmhA^Z1w`ahP}Utd~?-#I!v!!1Oqgw_`2&2e5;I>)}`0eMuA zmfL-OH*^j2O0S3}sYgMA?ju8Lv%%a@k9(%P^ctj+P5$FbV&It5mmfmhCv`wBrn_U` zmCkNCNcg!D2x>g~@14HVrg#wX`|3Mi4;6}wI^ zg5TbMIau)cKF>f;GOJnbCPCf-%a0U+2jBjZsGLqtjMHfF-kKn><$*vp7nVe~0?wNi zySKGXn9UC^VM%tDB0EyX<(z)?CZ+>Hcz(hMloWbq!5j|kF>sIxvvWGI-#4KrMw#IzCq?U)+&yw7-|DeIHeaTGkvP=z{Koru;3N-B zFhG3YZ8xdMbvrhQ*KQob$|K$iQ`C8Y|K5fED%M*7n<8aOS!8SNqh{*2V&4ixELrOX zv`%J$vdI%@HgO~Fp7ARJnw&;iOo&kvRD?<)76Kw*0g zH=sWlWHzr)pAetC;~xq)wMPGnN0%V+1Qb0!1$OsyPr$O~6V5fC02%Z{Oo0^|eBB`P z?FzU={{Oshm)@+e{3sEI53-Uub}gxRZGD+~ zD`2vmzj5@gr8^#H9JlcQx~BOAVne+%2{EG(xBkqCF%_S}NKG#zDf0Os$L&3IVgq^s zy^lzsM1bt2@(($`FN8l@Gb|;LSEkBv8B`jo%mXd<-5A!)xIvg85;t3O}0`7~uoYFkh{^ z4Cyq~AMf|K&6jh0f*}uM>ACjE)e8WYwg-2lQM4O9oG6V!=WObHIpt zv>If;a5`Y}2ji3b8elg!PH<@&x~8zZaeMp*5SaY4#Qa|yf&SO3at(XE0v z%!bw(V7lslioJ4kh;8>uPdAsFzKF`R`l6TMTch#b!EyCc21Gx|4k{KgofxFXOH*24 z`BGqIUh9~<_?`Rv{g_#i*MybwI}fm@!LqJD->Wt-S6k|qKy?E33v(p8FwgKOY1AUI3pBw8= zP~S|y+$@NIOS!pgdm*8YLK!u5f6chHmW@4gf)QEjp^W7~3 zvNJ@W-)Zr6!!AAii#PAj?z$s8%Oz8;Q*W1f<3sV9X@$GH3$0P#G^U03VGFEBI#F+| zfL}P9KG#zajVxNsIP-0!l62corIj4=M&vJkD!G+hC`nV0 zhTN6{)zI=Tc<&uCb}HUsKaHcaf6Xbuhe~HHnB9O*2V|QGaC<)T546Z$5hY>4O%XX# z_IQQdjcb^M0u={>8m?z@&{_+~8(T1zZ#~UgV=s-9PcI_A(3~)%cwl5S8@oapOlOum z)>uQV?O{-iBG5W>BL;2o(-{q19B=V_v3~mI$&Oy{Mmkr~+s9<Qs!*)E zy)^<2_^e3ZB?tA|9yS&hwLc9zg>+?TEBIth|F_)#CwyOJpL4!%!MQ6UKE7K@)+m^G ztR8wrLB9X9?mdqk403;lg4V{we_t zyusSaq??97XtlP@Z#=;@p0#%BA?a-ni)dbn`4|OwdSX&UD!C0P>S8HpnUnDA}l;YFVcc@VwUVC!ug zH}U${vn~U-=bVLLeVFH^YqcZ2=ScdPhK|_SreAq8KEMy|2fs``^5(pKC{Y~h=XL*e zstJ4A=f2-pcSGM{gfM=O^%OdUbH@9nU#AX>2GaUw;%cAqtlqa3*V9$dl044!ez1ba zFB%y*V_)H9k)z-CVyV27yIYIllR^QZu9iW{{f}enO3!x(D3uqs)DeA9V*pn73_Ltr zTPwkYw7&EI>WSIl(6y=gMd&!UJCVGtRTnb*o<}>wr|K&F*zEdl!DNU6`J>?kVYRgv zFFFO`J2SJBZ%YL)oF__A3=vyk!0bUkp72vK<~&at>S#hd+RqewGGJ_rhtq)dJVxpf zpj>uF1&L&JPrKJR;_fX;h-OlR@p+K|2OIu4^o`xCk$$!gMgpt@&p>_a&WV3l=6~RX zIvpKxGc1LEg#F>QPzQ`tR-DJ>+^hxar5}P@T`m#srzzHvI_}4m`Rxat_7A+4N(LYm zp`Dya=G(RRPC^7PobZ^-7+aceXF5-~0+bv;v$}X0@(1_LmC55%ujG+M;NTyYS#_h% zRmQ1uBRNTm?yVzLS8a+~RY{mIo^1;}d-~plzA+2r2cT$32X@D88@45@w&$%ox-%Pp zx(Z}7R=Hv&{euB;UeYs~sM4FUUTfMLC!;rbH{G{hL$uLQWMqAH9mS#RQ$~!wwE26h z$_f_ClKdptf~UkIsR3Sh9`1izDkMw*B8nm3c0#nP`-oYIS^HUel31L<1xonF^E(5M zXbc>sh%7=lTcA^sgi}H@XbS34>w9qrToHs!GT*gI&X!MwRi;p;_z=g zqGlbeWGMJX?>!z)+9Sr|m5u>Dm`-o{+_q%JJchD?HS|e-`&HF(T=CyW7_E^yC#`02 zQz%8EVJtb3quM3xEuUUuHLN8{zawM#<%9TE2mF_H!F~e;xcje7~nw7{I9i5FGwk&2{V#&1>iiN z-S?7w%dN}^xYH$1)sIYaGJlLuxpK|myY!l+`<))t69rf2%ziC6+*q^goeZrcOfQYzBTLQcdV?>^jI8mPGc1tMoazN zVwN5QJ}6&?I>$WD9Y?0Oit?LI8d@#LQf|LmGsh#lkTi1K{b4?KGO?qRtd7&`;IFv8 zqY|z;iO$}Mug(f&VVVhghwvm#sp$xU`8bHeBpQvgzUT0EHaN3N9jUm-a9t|E1Mu%T zzN2TYPZRX${<6yVgR|s03f)$u*)%l0jNgaFjxsiW+17 zH=Jy-_3}|%G54W0_s-XdriZM)GQ^N&yNi!F-P@!#(#Bmc0DZxjl{T&Uo#wu2DoGJu zcboJkf~WXeQ7$IkAgL0Rl#8)$Lsfyx%p@ z*7f4hm`dwjBEpUex|H-7vL0TjSMYiUC0+Dy3+No9ZG2f=1B3 zWoRH9lR8xiWbK(-TFo=#~-y=ju%&3}z)Cqnbqg3;fi$B6>uWF6{? zv*@{a3GeAA6%jg89k`F#(yF_>+xXx?R?@GAof;Z}o>)}#s&kxi(kX0G_J9JxH^C$P z&j|>=HIYL>m8ZcvT^d{gA3bhqD~t`Rjt+1!kB8Xld|1>C1!&(`lZko3)hu4mAGM5? zn^a9*Yt=f-^Ej%27N=up2epnMRh3Q^kDrE^tzGCi76%{Q>+N536AVKtspgDtF=ls%ujmk5e=zvikjWzpg^sg4MbnOW0%v@q zI4`6IZ)~*(=wSG-nOmok@=nG=NsY8rm3;;!i8;YJ?x_jT0AZT}>Im=W#Yn|$Lr{Fz z_4r4jGaeDalP7*RG0^u}{BUZ<@OOZ;VtrJw2>MeTuH{&xa2+yBiOE(?e8{Q+IUafE zL-o<_nf6)MUorMV*zXi{H{1WYTmRQEJUJkKLfkLAgs!2ZyHd9;CW4XpPQ#_?1#@}8 z>yXg&>w67`b$v2rD-q9xLi9C}&A3%_eKfrt1h zp&>I_j`O;jwPdGZWywv+fFs+|n^(DR`GYS|a+A_tbFBeu*IPZRSxe6@7%ea)Wh{cl z6JLH8wLS-leYvt0(;(@TbVHbQxP92@!cCHWdAs+dKe#y~KOwQuZ5U_inigOXIRgIW znJ7q&`Hz4?rO)1p;^WR9J6M?&cDUq@WO2|@CcBjj=k)iEIhgCT$aIh@;GZcLD#d#? z$-FybHgXRz_Y<^gh$Z72*~-4@2uJ){&2z)vz5=sBc(QLYey@Q8$Bg@v8?*;C;j**D8hnTf^e>BS^f&shh>5oLYuB#O(mXc3TzN5_ zxpSM>g0OX+c9)oVM!^R#^u{83!!L{Fg>!F6K&k7zYl3TkXqJ`gdy694EF4_hEtvvf zyq18NNPUEEU#M?2vn|)6O95?Jt*i1`lgQBW;1q|Be!l37cGMLE6$dC zAp6JG-44Kb{DTqDXz*wDf9(VGUu~@rOZK^;Tx8|;AtMBwf!U8Gc)vZT+C9w5%>8)N zHL`5A3x4B6lBPA@TUZo@u})%k)`HF77)?xX-Of7Q{wYGV2DGLH#+w&0dZbH$@Q*>+_h zX7UHErg6td zy64@M!`BZJ7SoPu5T|!~V?vZMy8}-`u=l;EBb%vsxr&UszVw)&z)dv|pNCag9_F_+ zk7{p7{>%xx7jX#iIskAZWK7N3pZC8JuMAWssB?JI zuS@gsa@;xP6gF~h!4;4VHCOi>Z8W|0To9*`PAE_b(yd$(5C{y*{#H#zL^F`%`dQ-m zpH-rs27QSL8Q99Fahv|cUE}ET{0jW6jg+7LbYpSo(9Z7h^l!;{)3);`u@7Twmjl8F zrDR7s<>=}=q~>lCTH*u`t?B74Uko=}U98E?9G)*pet(nfePGH-2pE~_;()qap!1>| zkBHW7DRffyfM^e)dd^)MD@`Fz?~HndoxaGS@X~kjeEi4!-R!MG{1abSivUvVGH)addm3 zRgQW3MrSlYtll`3cRF*_PHn26enfIna>Rb?Ift71v1dtw)@7R9g>Hyc@I!qD8^=QB z7+mfE=Q|IW4)Q#s&<%O;IeS)tW_eU5Od*V@hSx*u23LW$ZC*^>Omk>AdVJxv$LQt> zw=;ma_N0wug|ocf^k=^l`!vFF*%i?|B=e$$&_ z5K^d1cFOd3H_dkZEOqe(IUDah%f42dXlat=KBN)NHMcP`Dyt`?etn%r5Q$_f)46Dh zroYAqEyy4fijYYSyB^<90`e?cY?&>2jE{a}dT`=r^xZ#6&PIfQ5n}-llWaFE(yo&? zWzuCh%a9tmXIDNjArn)MNn5@Rx^wDU2DUBfyus@3GH=Cwh2c0mH<%y_X)!A~3s*g~ zR-E0KspYYAWN40!XHo%Tt~Jgpi*@y5s#_?CpnR(Up%X^*z_E>E;cu454`83Af=GRq zp_g1(9y7W1dh;dno1Yuo^Q`XF66*$U513WiB80AHzwHl3QA)sz!#kQ@#x*4my_wL- z;ovNSBQyjgk8Kd?4(lAy2aLLux&fYn_W30`A)3?}hD2zv=VGJx+shsaSqtjbJK+HB z&V#tpJA2@2i4I?)vi&M{3(YzRJlwe)5UX@JM`f?rDpxaz2$o3)EQ_5!s345yP6K*nwW!iUET;*qP=r&;rkWg4qLAD$siW4+cZv zdi|fd|D`5ZU!~^(lor)tadv>xa-7SD|Ne4uvp&7w!{qze?e8adqawG41*?vEyk%u? zYhE=_&`~~C;@$dezwrb@uX^z7dTU1hN5P#OKPwOUO7f#qmE0cohH=Jj0Hxq~Kfit5 zlb;}LC7N8pGeDK%FwR4sMHa75(aDp+8WKrH&Ezc zml*-rvM9(Y*E#@QU){|VKHAX4XMtg45{s%rS=DQzmw|2zinPcJE|uYR5dX|n)UXw2EokCR*)!uf5oi&JCS6j{;1I8w!i?`mDY zFi4(J$FuOkHAGe%-VP8@xB>P7UA$d{f5%a+^IwV@oioz(d7i+80tPvan8AMM?&hVe z#>BJPh{}@@`*}r-;&VSxyB4`B`#LX6R~Pu&uA{?Eo3Ba?K$w9zF=z5HHa1(A!Y-5q zywh8?%eiBdUFgGiC7j!Ob;z^dBVOfG8p#yzclOQ+t2|^h>pmCvhD+3VjZ$dY$V$}U zB=@l~1={6Siw2s#74WIGg|^h~w~aG9D2*u07kZ)^Wh!-0#OcfS+>Cd#G1I&KbvNftTZ*3Z zaT@7-!sj+P@~4I_`WpgXxVSiD*v(hZYJE|ycJHvC`1hOcN9iUV+F45Rf+fYMu}R9c zV%KuR<)xoL&0d|y%v0l(pKk_CKld+Gf|0TC-5YO7eBGVi69nz`M-+6eI-6TK6vySm ziS)UL@2{K7Ro$;QjkKold=cIN=qQf1!za(Uj{SVd!H?Dha~Xw7Dj8a?)QJ=P z5K8j_jBb+W{_XD^4Xz&`R!yTgap~;d_Dnyz+P$$DiwT22(ts;6;AqvLoYwAs^9tVb z8qOZ&Abg|7M;E`9+_2szJq;ux7^oc;@#x4BafaV?QQ#T(w2VBm`s5p%0`}gF)UN1x zB5TOIixSyb@XnsR;5P|T)R=!3$v^bs&N$+Vx}T5FSJtD50ms5?OXBhK@>YIh=wR7L zt5#G}u8WsaK#QsxamU$BYSyK>+w6n4b40r&n#|CU(525P86wsk=M#Gxh1)&AaYor% zX1>)~h^su|0`ZXcr3_NvoX@af{kg{DDuPsXSBTKZxr_n|&X0*^J~!rCu?#NN=!v-HR%&Hge>PtuI%yW{t5I#Ja+J*{j4)2p{s{ zg-&ughIxJ=YCL?zds~!b$d@+Fb^6?J4CLS}zR0&V&c5~1|2kQ}Seo7eac&S#w1p4KZ;fgezO{GTDUEKm-`eG=KF+~Gw>zwlS&Qlcb*ygyqAJ!sv z?Mz4NT~go9@K{W9*bq%^ye4uYqMR=6Xgln!$~-mM{d&sE&o5tQ%@#6NizA`N<^^`y zmwt~)yQ`n$x)>5_4S=Qv4^s(Q?d;y-a`6HuJk9t?i@Yy3fun-Hnpr^(%*$m@M+iAW zVF~wjZj($>-fal(c|s>ldQaOivq86Bx#_9T#@Boi8G)eI%2Qh0Oiqh?oH@;_UAPS= zD;C2AI!Jd5akCi1gAI;FogV*En=_K11q1#7a%$4{8uzW50Bk&Y+?`=^8#i=>Kpf@7 zhK5)&X$}N&AD6bBv`8{`CdIX|Q-x%C!D?(qb{4R8!)K8I(4LIIFAZgOL^vCN$E2+# z8Geiq!rT855zc$=`|T|^(HLN@4cQiuEJC#hRPNFQh7T!BefT_W-kEPk=ALL2 zSL;L=`ZL(5bW87sf}V+7XG})Gy9jXWd4b9s11qK-{hEE#UUppXIu`GLI7K`t_s)SS zfbg~Mm2A6kM42ptZ@2eVr%GZKBiIjBz6TnugTXn6+&&xPK3 zJ~Ksc%(|TQsPt(xwl`&#y|R8BpKAM9;VMTIe8Ks)yWxC(jApZWi->9H*}&V}DA zKzQKLYJk)N-oVWIYa#efF#25Vw-X8xXSj$J1yu>*E)EXZy&%j*y5CJ*Bcc)6Uj^SZ zj&*YUHI%T^(coU5>-lzglDv_#oa^n`GPO6%4!(T5XH{oa8w#C0q`nX757iux0t1ff z^QtLImd0)N&&(+vgxfuoJ>ZM%LK5qmagO=%n8Xi%V!GsByd&$3GK+kW{S^ZKQbw9V zod-slKt*V3hqiNOxNVt6r~W$90I@k|1kB$?ZJ{yE1g(EC=)8|DblWTtDYrw}Xy+`l z{2}`vjY)owr1mduCRrc3=@{DyWTgKLHgG+Bb!BG?%6?*@13YHbu;bh_y&Gj0%xlA< z`OFc>)1Rjm`B`ehTbc&ou3z%o((HtO4;+o&iryBka^%jO%sEN`#;d=znV1bVr>995-S)n&*HZzik!B)8 za9UvczhhIt0sL2e;>R%F_qNZLi!#1IL`#ZSwR0WqZgkW-DWi(;hhgCRXS2bPsu6r$ zqoI6~vGB1vJ)!!sl@&8yofMC5{(gdmOvl|=+IX;nSg;^W*ngX{DuRCYng^f+_X*KL zlc6wHIt|Hy0JmEG5B1(n7K!3nNfmdt@Yj6Ssb{p28JkkFmc<~y0AdS^)jh3m%#ORO z3uN^-+z{%FhGB257e-rUbCi*sUXBcExN$Xu-!hWzj3Y7#)#5t-5)=Nv@V5Q~E>TN}W`OK#cQ*dznP8Vi zy$u_ACOQ2z%E*v`XA6h}s^EYhDA$dR2d|)_D%0fNuCp1~=YtJyH86oLeUL%4d{4kB zq2~qat|HR;E+`;gt~?EMpEIQ7t{&hDR%k6_U3&myx?45>vs;_*1T@DRGnwOB&(xCtyj{lrabuK45W{vWmtB-@Fxxe6oysDgCXAPGpYy)vtJS9=NYYl{zejHWdaC9| zsNJp7-YLbL!z^%N(s#JX{dK;Gn;+NSoTnianc=$5#g^RdN7nixsZPSvW?!92DnC~& zeehgoU%78JO->Zu+t@x=SK9LB<;ifFk;c1s5lRdL&*BPL_0R=K=JuT0j7e*(R)X~e z(QeOBb`F&&=orO=XKLw$gYG4}1ysoUBeeN}y2~PU8g*4ks>(4*?v&+cR?;&~#NZL@ zECH9u$dqM*t@czImKlu~8tlr9Sh#teV*KnRzlp*`EL%xcsrg5^^5|YGdRGi1%t5vR zzwX{Lal3yqHW4Z)s(utOW|03rkOQq4dLb>CaZC7XfBwFgyW?iC7k(!LGBnRygVk|J z8;Ldcyg^V?QP}QISG^Vn_Z)sXJY}Wk6+4a#$!ObF#n9?bMh9|cSujGVpx}kAsI+?+{BQH_UP%&ccEkWYx)}Wib zilh7QEUte1KIKe#bi%vG)Wk_kAKM1Aai|h66X{$J+Gol+Sd4 zr4Jc$R-w%Im6D*TvJF(xw9qn?2kbUU+e?3@o#wJ549Uyg*8bTk3oQ7hbj*vTd?DRv zh4nGxd-gq38qU|GSNg3x{kQoenfUu~5`8Jn3HKV@#j@4U#z}E7!<> z^iGJO6!*wPVZzPv{oB9(ZrRo^PPnVr?w5L@NtUX4dH0()A<_XwP#XP~u9j)uvgWA$ zo8Pz|+%I2VMfVF!#SE{A2P?ycv(!}V!3tRlR;m(Y))qv51MK?${QUd_Yt^?$IijlI z^`lZY&V@Q%!i(~l9LK#ISWuCp$aRWu$DFgVVzs%Jq1B_%%(*}h%( z;d3tBwmE5GMu(rf_Dy7`IjPk#{gUzzozJ(828mXcS=n1daUE~jO>g8C~81^RnOb1md~IP zMfOCy9%`p+a{mYhs5$O4Bb3!j^sUdhWlU_oback=-pIsl2Ho@Y+T4%>+us-^Y?dP3 zz32hD3;z(^@1n|H;LM)?p`-TlWt@l7RM6oVhvaI$>Dq&GkclbC9aT=tVX|6RGKrTK z2>tMtu!6S<=Ntv6N<#waMZBm%6OCahX^K_?K9jY*MeXinJswv*tgWG(9jEi#J+G|f`G z=sBvZlaSA(bF5$t1UA*a-=L-N_a0Buu5cBJvg#&Yk7d%hT|6rKXkM&#fXyetXD;122QZ-7BMe)ZuOFtcUjy9Y3J%dyasG9@tTDZYx%9cFjwo-~ zdXl_HyV+cRZMaxdi~GY9F_ISDq>}$LJ)k=q2{Y^!*995R4#+6YnT~7SbdVfj)CDMk zXtTM7`wT+I?Wq-lo|qlu&7)__P<8}WeTraaoJL;Pw#`5S5Gt42E~s-!VCZQv)Z2fu zK!&f>O6?#YR&7%JDY*#!vsaGi6JAYLI?}6RN-XuNhF_)AM3L7ANP0Qnv42iam598OO#`R9>94J0%LR}_*^aI0t zrJ4~PbQfYeOu~8P%Z_AY_SWQaKRNWCecV)~3}IzbL&|b8WdCCjgGN0pu95tZp|fdN#OnoBnC+5I+JRoVp2t8K-33Dl z>05Jabk$62Y~oft73b&Hgy07dz*gfzaBL$Znt&x=)uZ`wHezy|Qg7A3(sHgOTs}pf zG0UKMXL!q3ruEczjZRvfVpb`jM;L}Yo@}$;s~Ahw(;(F~(tCM=>C_qj>-D)}eW(5g zaQw+LxBBoYPC|;Ad^U$P<(`D@$o2bv>oBA%c?B<1Ti-BC&~!B_3t^^@_7+I9#%{z3 z=}mUYEqc|QiZ4o4umrBuBz=kDg0aJ&&DZ@ed3T+NRHHsV$_gmb-a$1rVb71;wjCd@ z3i(!y=Gmfzd06n*H3HZa5Kn5hoSUmrG}o#Ug?eYIpo55&z;LMo69V$A8GM~t{Zn~m z)DSAVbo8_>+Chk&c%w6JH4F4@jDB4MC3MX%jYnWW9-CN&2}GYqR{|2bW4-R_b9^0W zI|MJAsE~6&eHijU*L&;k^BlRSHPMf!4CMVZ8;<8kHK=IR`+d33rirX7ss2KBmFDVF zuyxlPw8_~<3Sp#}n`zGPjNZERaVo`@DL^*fr2b-%{Yb;tdNn^*q^syJVJJFYBpc*D z#9aGDzcwlyH;&d?^A}(Y3RmYTUUV8_xPfSN7;PwPMj2>+O^PJcRPaj2f5CQB^)_x< z+0F90-$9K0do)YYXhjv2B3k&t_VsaIcZoyk&j7o^G;!ovLMu7BY4rp%0Y%sL>E6N=lg3SzEk!}nMdT+WE;q_JC8UA=JveN zx?K(WtXJ0*4@@tCytR)5=Ao!E6u^8GovlLg>bD?{vkDiD(q>-Si|lL=JdU|txei0q z8-J)-agQ(O7yw-6yXS##-m-i!QM_%&ybSkgRjLEe7;VH)w;OKz_}Jl}$0zGs#QRWt z7*rPlae#gB6culhO$kt94#5?rhm0>r87rI?{d>w?@9YIT1nsqb-`y>zngYTvvyEqV zD_2tYiq)AjOyuHW#>H-2w+UePJ5GyCUOvvQV?b9m2rrMWMsUX1#RO7Qe!NZ`NS1!y z8MI!XbNbgA%tX(gUYBU-?L&_BM}iWB|2|;;3kF#HuU4Yo1bC7T2$2k#7wDTO@!P=I zB*_(?nm^I-!~s+OoYr;Ht)pg_mKDG?WbR(~p&ILUjI)Z}eYRGdag7eKmcukza${1Z z9G3Z5pU|Cd4F|?AlB&wk{lQ>^Fm{iy=5tA|FO1O?)M&U{dhvX;a<8tkO)M$-q&yVO zYY>mzifeSI2N{60oM}_RmpSLThqL2kX%?;NAzqOcGs(A5GR}l|L;I zp$N^pSBmg4i7RVlzVFkCJzl+n&(G8Ixm`?_=JoVc7PGEu*wN$Y9N?}zn$V4%NY*T& zM~g;gV)m`cXK~&2oR|YPVa>rlS4Jc_?s2`OK*jx3Ybl_D-K+E<=y*#btMD4mgsa_; zc?nngA?yi?1?izYxbbCxNeHK1^{(;XM+^aTo#PDjcs;!$AQmKM|ehRMO?o|R#ilsc-%n{Dj%s;vNb z177%#2gZfD^|^q>94Q()$4dF3@6B8SN2Jb*)1A<`E>}9wrSQEXa*(T2B7I8I8}{Y$ zPA_Y?QQdZy?04a-WJF;{4Mfr0Ai0D&Sd?}*t~J2gr()Q$EB1E^5zcU#665D_xwzs} zJ!ji3IzZ(XkGNjRh(%Ill1<$Lca-nfbXK>OeaAt2G7bcbZbpk>v;RCcepd?(es;By zo~|$L7=E<$%JbeE2+8L)E-&7aD6!tRz|Tcx+`4TN&qx1?;Mjv(5j>s(Sb-*pXy|D` zoojE))bcpxz*r0~Q?O<_y0~gMlvfS;h{`RvXB#5956ihepUgpzyjv7c9Mj7=AR%aq z=^+xN#bMidQA7QYCa9;hF#qFkoNH`Ep21ngD{~Ye%{x9G=`qRpbPq(KU8Hiv-?Yfr zyvWPA+3AA&{zdWQEpn;Bp`DSjyhjL?^DgqEXP4EKVkComen1opdeFuyTak(@5F!fU z>rS}SU*v7^qmtvq5-ih1D=FR(30R~FniWzQ`m^Sq_1(`Z-(Yv=lK+6u4@^YsNas#; zW>T{XL>oy)9utmW-2=N1Q=^aWnxi)C8z|21GIlw;> zuNi4bAe)TihzOmqMP;&^bos)1$`z61l8fJq(om3XLCL{G-NGfsuvXSPrgsTf5~KM6 zk_FGu@a&xQ!*Q2C7!uktT74UDkJ)|=SBRhiZZMeFa{bE(K#E)Aa^Dq+$L2Y_^)>MJ z;0-_fPhdYVMpf@rnHiJ&$7yr=kX}k=wvQy_{6BnsbzGF|+BGVQVxR~jNOvRMj7m#) zgVG`02#kt!cXu8EN2*jz z@4a5K#kNWLMwaand-XPDa{Bo#Dzl}vZ~`Jpf5JJxt>gdNcPNW|wspKUwf?iYubt=w z&buZuLgHJSBojTfelQU~!;d&mquJ)SvU%T!Vf6hq%-PY#4B&FijN&IB)B9`nt8LS_R zdTdhH>i!AVnFe6s?-DV#eM53Fmh7O6 z22H6|V;GSzk+vjH6^ye%Xwtv9$h7sA@4GzC7kOnb9xU-@yc;X0c@O<)QzJ@spD4z` zTF5<5pSzT3J+(wn_3H%YV^f9jg>6d|vgW>_8Kyt1z;l8LwNoQ-pUuVA3;IK3?_fEi zwz&$t!Sgdsa3ht3Ess;k*|mqy4!z>4-3b8QSq(AVW@tz&n1@Z;DKL0`BcdD*e8{5C z*9v-(=?+>tb~i!&=MgIvN@d_-6W07D)T|(=aS5%&vi+lxhpsc9X9Qic>w1r1|w;ZRAEO;9v9dp_3) z7MV#)AdT8V5hP6Ugw8^^q_$Vetkf_A&I$z>Gq|9n{%zqyl9TdA_Cp@b05IJVg>?cb zb*;yJ=1w)Ej9%~3UNVq6M3dRWms}@_-!<=km)`CQG~v&Tt}!Q78-Toebze_8Ij^HJ z-YP_UHXbo)5b(X*OlK<>9iF3)HcfS0cT_?@ejTx@CEF6GP>Ui~mXy~;iU~jO3aQ}f zzGNVM-2n8WxdV>OlSM7q;fZ?%FJNDrP5_S0cM`xhZSGP^1VY+^g&i zmI%_*3*g_x2$Dpfm+=&T4Q9-!6J+Zx-Ce1y!)ZgYtED(&QP9`Y%bu&>LQp`(_7AE# zG!M3MPvRpIJeWIJSm^s@()nF*6q=EHu)TBr>(~-(JqqWY?6^3AE*a7ddKNDn-=BVtUkmFRsI3;yZypP3T*Rq>~-# zRd74&%5fq&Mql}xD?eKCFLgi<*nvfQE-Eo6L(hy{Jr(EM^ zLNSRjP4Io&?~vn_$|g;0I2J~Oj=C`%w|U5trkZ$j?CS;{SStY)kQr^@ptIUryA|=p zSps-gi9kI-YrpaaEq2T-k8CCF>Et<0RmeX2BK4n0y-ufaCwh}QGst)0) zPOIEumNd3FX^d7lnNuq*p8v?HC32z1rf{E#YYSFTKeOW8xb)0_#5vE>knnQW7EpK< zX}L?&>?AMr$a9=+BW2I0=j{Gu$0HP#JAoG0*2sjsgPQxDVZfG=_m$Q<`Jiq`ns}ex zpRC7Kw~md+8i4dl?F)8y+wDXSM!+^cmHwJgR#;V9b}-x7UPjh>fyrl6jt#){`zNf0N7nL7}G7z z5pBZ2ZZ9dWq`lO;99hji0UkCF^yE*y0?}GD$UO8o#2u8d0~!TVfsVe@Xb@0TklDL$ zU!SS{tke@`n$;Y9cRH+F-tlHoi~YL{G$TdJ&U2FcXSc(|{4#KM0!l&pUV9p_ExTrCp!GC?)x z+r2V(_gX0#>~M#|-q=&D%G11qiR!nqwJY+~|AZ2%%JYGd(*b-)ZM0eCa$Q;J`DHIE z((KW*_a6c~*+ATYymN(RQFy=KBaP!<;VzcM=ThY465izT+`6m1**wjmQ{jm) z=Nvh{f^@mgbskK=GqWJVYtbUuo+Q8&{MN8%3Cwi)#b9-k!dIiTJJ55?mi*fwneEa1 zj4~goWQ&|d9>tdtc=_S0ZM3f^KfvNqGe^qZQnMBPRiL5 z+kjo{Kq1ej{o7sMv@o2-DX=H7?!r%v)}@$+BdPuUqQ9Pr&03^LE)@hCP4>VF4Yu=c zULYO~zF4@vedorS`}J4R!a;U9FS-A&-aZC;*h{ORgbMNpRNOvIv3v6HdqzVGs<<=s(Q$2v3G4cYLWMOl5leZEes9j{Ti61x7ycz=<(80 z)ejlYeP_*`NnI%YM=3_z-Y$JCrM=XcvCh%&;rH(mS~*UAF;K&(GZ2=3`Za4jSg+1e zO4cymRO4#{8+@7ln_pKJjpc?BQ%d<;Hl->6-Mcd%#Z1E2!V)q0BQ zlNrknzbnDn7gOqY_JN~{V`5-dpwfsa55gSaT)DUztWY^MV}0R+0dGI`OiGkV)%Ax_ zsn~k0svK!+?wFxNodcNQc3)B5&HBE=rKfiN;DGIurIkcz<4na8AeKgUb+QiHZ`0ca zY%n|l3gJKK@lX@s&1X$A3gy=Q;1lWGfO6;$A7G{~@%J!10E+z2hCUUH7F_k2c6sWlpQCg_NS7;z> zX9=|1jF4q$8Q3RO2zb|apTLv{q+0k|120>j=w6kaU#?Vg^qHklJZZpy*3AUq>yeS~ zucVZMV^w07HQ7E@B-_!mB<1QOyIgT*^FY>90#vWnxC{cNSu&@Gr$I&5bOuK4jc37f~zI6qT>`eW;N|#q{E5dMZH-K*{PkO z);k@P9DDh7a!Pls}698G#GyMzb;HvK`;=ZEu#U&ezFi6^;RajmZ5U(U8N z!}{^2Udsk>DGqKD+@O0M#wSRiY0J-%7(SFa_l@hVg8XhofM{`2b98H;{AbPTm%nXc zLwMFjuax2+ZbqJG&DpKKy8LNcqQ}1fnt@a_#rS!afqa@fx8)5}s}nut#Ic(hpGOH3 zXFLGFBMPxCoWF4kupTJsLJtyb^DO8G$w z-H8+BIo&s3jwKkUqog8(88N|9dKGlc4DKLXhguNW-W@tWqWRECX29%TN(k6ZQ--KI zreWxxS+UF@9m8eI&gHE7JA+n1Bat8Gg1g1s;;wex@^*RM{Eaq@LNlyw%)1<69|jbv zRf0ce6-WNrSNyv#N%Yk{{O;1xWmUr&w8zu`(*Y6vn?+d{(1LtFRQ+uEWO)bZ(kL_4*c3INVw;5FkkIp8Z$aXV) zhDo)p=JSQWiPKboa}ZrG2^Cz(ZWiJ~H>Dq=o%tP?u4(o>nEg*1`2Wj}|5;qf!H9vS z^+Stk5-2W%(k~bcq+_%Elun+ZKsr}EYgBxi%Zsdo_jfJK>p@H}7Fiu1J6B;5b}){O59 zKmD((C4cD?{;w?S>`lY>#?KQnZ{64u6)BRc21sh|W}JhUWaQu1DBF&5BC)WyF0 zk?#KMA|PNW#5E^;tMt*)FKq;!0m4 z-gQ>LZ1`me5TBLMx5fc0^y48;R+ea8b9rnMX$gCrJTRtyh@Cb>H;x-E=K+x=Nqri6 zwiLo1X9^zn3G^_z^U2Kc2jd&~)|~uh&3&KzH}f=K0#t2c{SG2(xh!yQ6X-}#yzF)A@W3) zCp@7baVO=bGx_hC&P7>e;*|cml)eo+99HgD%+Hc||3+;cCBG)ApFz`DoC+|SkZc-O69B97l^6T=4oWlZ_ev+knymZTp|+IKoxULg{} z+ouKujxh#N#}0jB9dcM4%4hs!sv}%&@*owlRpVM0dp^H4z}FMqBEn>zONBX;JzjIW zrHO3I@ELS^*~hC12;!PVG_o!Iv;P18MG|eNn8B~XEz)u)nz=skTBVQ|n2^U$ZU;sJ z^$hEFU}JFER&moOq@_;vFNON8W$ocYUE!OP=HgNxhTgybJ9hdAuY1UW2loV1!Ai>U z8QB$Ugy*^~xsjVvr~0ALU*X-eY{m=UZ62$+?NBPUipSw$5<{x+r-gWt&XR^0sCsK% zeN&=>E5U$9CIAGy;OGD; z&IX00_C`-tZ75Bi(2t+b>m!KsGvoN!w)$v6D1P3CiC-T+;JBT>Qn#dgy9Mx8^lq6p zhzSxcr9F-A?ipZz-B=YDXLJ1V8DEl)IGHD7c0=>L+7E zuh$WkvSHRaGX`|x{y*X{5C;dz-7Uv=(VVCn8JU~u>CvP*a>($RYY}xe*hK7rg02^8 zv1+T9mZn_AFs9X|n!N-Bl;^x@qrUFCMa?@ZZ-1bYyK_Ki{L?H~R zhI_Z45{5DGg;Th*8Jh$yIMllg5AX!DMDkw^BzoV3_?l z9;lt$F=aVcb*Q8Z#ZbMD2*s7~jiAf2KE3#KlTY+1G!A=#Bb+)<09SKZI)AA^qd8v%8%5+4CmR)H z>1cz8d3>=rQ=rfryyI|jcYA&3@@V@)+R2zr?Np-Ww_IIGBMc27r|D!l@-%6WxNx~7;A#tU(9&s{d9Y@ zPK!VvwlaRoR8G5pndk{VD?tqE@dCZTtGhb6+ouVFwhAj!Hoqx2c5r}#L$16aCZki~ z#Mkm<^Yg_dEB|%bnHLh%Xw@sFN`|Yc&$*dk^Qh_Qeks6aN{R{l6X6}F*ULDBa>SN6 zgt`M3clZCIEN+>loOa|fXZr?LzV3*{7S!>$U;o%59x$6`pq6+r^Y)^u$c1P|)~s0z z@Y`hECtaq1ejtxh-Pe}A$$^q>#g?D1)uP`fCR}5WCl>plww$V1By|_Nl$^7&oQ6md z6n(h+wjYr5TKod|4rQdU{7b>`cZ>69zYh&=77@|eTT0G=<-1c~^;pT)Na3*QQ{P%X zl`hA*fE4DB`ud|nM*Nyhl|q<0<6g}2>eTM9D z=nb0crzbnO+gz?4uy z#tMr(qh8O7xt9XOKV{H8P<)!jf#Ois)jZI%*kCU}3T^j0#uf8!{5usCX_jeah1Vp? zq^j)AzVv7fGTT9TZeH%44aml17Gc!XGv-lEgF?a&4UAnnT5?{+)l#PT2C_f84@$`r zy|Efm0Q|qmwKddU_TV3QNf)P3fzvUCX%&eqroijahp)2CK!* z;-t=m3kB-MePO{9D)3GF<8Bk&O%x)fU@cH#TPn?2;YI+hU-q^IavQ+0B1Mk&;G}QN z5_3~e=elDct~^NXx&BbC7KNV;36gGvR^AyN>Wgb2TJ!K= z$0LID6nyrMXaf}2Ea0S?nW*u7qvA}Qwm;>gz=wi9(<@M2!iA^r`Bo}G=$NJW98L6D z4t9wkw+-?n+tYl?G$xHHKEJ`h!u-1_^X+w206MijaT0+nkBHbEZkpO4-f^Wy2{qMq zMGn;KBds*xexnbXUlvKt0g+Y_F>zc{Y^!izvR<=n-$&>e<38M})%Mb3=9E#Ya>xSM z<@oCynThY{q3yxx7cenDFRxLf(wa+~1IG0p`n+e)17x3-Z+m@}EJ~fOX86RbYk?(j zd;jM8!j^)Jfd#cTs-zgRC`gA1lc)ql>1Eg$(uNuUkpl<@}4 zT9aB+>nF^k)FT2>FNVzg-L7sd?zm;`GBzST=AIEYy_=E3&QlSc+)Baa5t2?h4(xM>OBPSX#4#Oa09HNeJm_vTC$ zid(6TBCIfCWRKNq7@F$_^WlFzi!L1|sLI$tpF6cUB)~}h?aemZJMMi)=Jf|1v+wZM zeF?}U&Sf81o$??<6S%t$b@!+xYcALq^`(#=6P8LvkBI&(L7b?RwNpVzM*1i@Ti;o zE*n9YV}d(C^30i@NNzkxQh3g3#Ag2Y_h=IFWr-Q5hZ#ZNVNM+QsJ3~tdnC_o)80*Nx!RRy9Bm`J&82;diW>r|>SEz;R(gt8$1-Odt3Jk4;zm2vqjliRkvs0{d zIwT~40rSi%Em8#uIM)*qdB55#Y5wERbQeLEIE&+<$ZGjHW=u5S5?+psNR|67>+H{_ ziW?m5*Okhb^o0|Mu_E)dBs%8~0B&bDs`b2=g%@p>TZpZ0c{5>aYTrSe`hx2mPoL^X zTxh(;bmwUe1ipgWKpH=a1M{>k3k1iYUavaM7fFMiK)ba%ov*Of7B4Z*>p#G&60j2` zQnmUaGSuAE5zhoFcd)K!dx0*+C?srDy+Py_G+@4({r7Z7V1%0|8L9_|C1;d`^XkuR z>uH*Z%~y!nI6WZrS2}!RQ%uyzjD;5+;2^%?#?2qjOTQHq&uEa;Ul)^9tB!bx zr7Zljf>>rw(N)NL?>k@nK8B9-UHd_Y*E`ny8$c=L3t8?GC+YT!PuqWO;!PNCpsz#e zMC9TZ)xq2uODB6Izh0UMlip7C3y``A=oL%dI!`?uR3zl?B(K46j2QBMlAAA)X(Y?| zdSq|)PIu*h&FPXTRS0wFx+A}KgedcSBe?Hp*MKHA`X>C+JwcIb_A6OH({{pCA^8tH zq$wsf(c>23{pbhBxTg>@EC7;Z<)Ecnkt#MSpwx^NX|Cih9;QK+E&BdvETY;hGo+9Y zKS2(pa)E}x&6B~5_YPzTrQ2bAt4-udlk0gGn4Yj*@2U90!RLA#NP)ceS{>{ow16;X zUrQ_^7PYE-Ki^T{pDE zVQR`$whfWq@Kf$rniE4bef-*nWNT~431e`-x&;l*YsZN8NEPe~b`60%PDQQgdqLSw z96ZdDeW;FfMB9Gvn~6T$-*aAz7G(edd*I}6I4))Io;g1dflvYOY2A)iZNj8(WE&0Z z(iAkLlf5RMm1hwdQ|9?zahJdxVyd$bkAMk)`G3SpIDCBA11maU{j6Sgl579o99jt8 zcq(WWJ1nMM-7VWiEZaC~L7peIkSb2>QxmjCC8!PGADsHTqnjZW=r$9v(~GxXv_d~9 zemHw_<01X$N5r4YN)0pC`y#@qRW1TchqxNYtpJ@MKa3l*M!{Av3#=T}E{NOIf&z*1 zJ7v5SrE~e(WQM=ZXE(|v<=`OYh9dU(@7Jl>St}|EOF!HY%$kvM!om`F3@Mg`&r;ph zT#j*ej4X#+nQZ7%e!Sx^QC0o6al#^yk7d>Dzvr1iS)s$z7cIDk7EW()W6i1e>g*|8 zbuQdp&Dyo4-wA6x{(?t!!^-LzVQNZHRz2tqt7DrII590_q3zM( zMK=5+jdAnW`$0RLqoRs|b!6rC3Reja%swNs-mR>}8)Xe829Ye@0J8FPDP8OcHWvM) z3bNRd{BGPxVO_=we|LVO)Z8uhasLSMEE<#r2r7^PVvuP{lGU-^V#W6vB8m{&u7)V* zK-!7?zI4L`^g3`_bg{@2a!{x6q~YVkDq&M2YHi1_N0(fOG_lJAGsUYoR~XtQAAHyW zdGP{J7qYCzAzpt(VX=2nejZ2$dtQpG)CQpE{p>{p7(S_CMxgyARZcvqrABf% z{FSm|;B5}DNwkm47J`1gTc5{~k;=~lx7jIBalruYd+|1LT;Moo&XvLA@@^*>M(W=Q z+@nn34;>}|(~rCll1C*Yrpe;WB14Jr`YOpbYn-!Go)Cm%Ch=tBO?Bu3kRN<+lKU|HTFr3c~E;AtvDK0_8kyl1~H^@=VD{s3(&@BDv2UXy<~Gx*0~D?ekgte z`rN?SF5_Tbs@C1Yjg8S*jSOEMa)WzQe)lSIiPRqeQbb8H;?5YljSy6E@nEXg%G{lC zyH^n5G8oy9&qtJ#We<*zE@gF9Fa|cKH^twuU&P{Erv^IShC0_gz52GU54fnXOd*i$ ztoxnZo4-!f?9x=-2()l}@)!dzDMxZ&CclZ>$B-0Be4dLhvSESXK!<*vxO2qvgDxp0 zH2)2Le^hwGU(UY&MC-*-C;gvX(QtY|7&1_0C;4{6;b2HFfV`xh0n6h8tRDq!!=+K% zpc}~|6VFPTju4dnJ;t0LALHj#ImxB%B6!oRIKk&^;InmX9&<4PxDain4 zsQ1C^Svu^a;0@Y8@c6N{B%^i|C&)JM$8b+pITE`PiQ+f}P5AuEI~{%yIw?2H2AU4V z2F~EpDiXlMDmWLp-POU1Xw$qYb2%K|u2hsx%y-Q9jY|{54U-^aSgcmtZEkMjg=|hE_rD6MFHgNgGn}z)ayekVMzJ-}?T)a~Py|g-Y%mvig%>r)3md>*{GOYHt{f z-ExX-;gblKCBT3tgz*i|ZmFl<5~u7BAjcT2$)Kn|QdJJXiZ-tHcJmNzrS3=GcL1i8 zRez8FS3wy~4m3zJRsm&Q<;ijJ#!w-@Os#A6Q*@isn=bB`d;-*}A`EKW^?m#KbC1SNV@^{B+PCoy?SP)`u{#nQB%9Gv0u89|e-v^Dc)GWLjq}gj1%M zXzuEB>P*0%Tgs{vOZS$ zK>kEM_I4iJEu>pK>`LE6LOUu+<(UPYd0LS;^`;lY5uOxSnvc~yT zA94fRhfM^+YZ!KL%4~4aZj`%PkSv__spAd1xv^XGJbNQF- z`W+k{?lloha~4i76Q|hAv_&h>+`iI>d^-ZxO*F7Tp3U+{FKx(XBM*|eESe*_HWE8DKKSYYo>4#^rcI-rgs7#x zpER1oRokrz6$mu&9qE74@4K!-eW}_QekR&uV5DAj&m8f@C-WcDW(A! z#PR4%iQvgK=UzroC@p zMWCi9rv^ZZz3QtokhPt0`|vcrz0`3p??S2;seM{S7AlOv?peVnH;pHD!~$GFwcOpY znhu$@b@^Of4e#)aSajppN#L1y;q4R=bFEwU(H^WMq6H_5FuAwN`|IDHU&&3Qt$pBj z(-g`)?fCs1;4Z$#jlLyxd~PQ27bxzm(MDscl6qQ`xc=7E95bN>$I$H*De!HNd?5Bj zv~fFQhL(o6!dMJ+iUWtLnzz|t`C%|Tc zb42mdF+VH|bi%(4LW!)DQ$sO7G4aq>jwd|Nx7}q%1|cR&s%D2~p~^b6(`^$~m{r{? zj`e3P&dxtAqD#_Sc=f-@kOGt!IrfXksEYC8=cEAuRw6QAdaZ2H_)(k25zTHU-|7X$ z7r-rTHF=!K4@ds4BlQVL(=$!g{Dv0j?R!KPj7B%A9jL{!xbHWmLK&%KIEByV@94v% zV1hOkHJG)9vw=l(CoP&B3&qMJUqh-tPf@NfxS!e@ZGk`P0f(P5emt>dOm$(G|5i0> zUO5hw7b8Qn8kPX~DEeaE4G79Aq=<`WuPJVHl{1XqI z>8r{YTT7(AJwQBvY{gQ_DoP720JApazV`Vp+4I|$foQF#f?2PT<+87n2*_7&2S$X- zL(5H=wAWUQML!GWAnwzAgf6A59oY9Sq}@DuSborGfpQbAy>eEFHJqpxnEg;x28UT( zvTTa_{>*!K9*VvTue3-Zf07_K2zaB;th0K(m|u$(-IANL7T8q1{!m|$L||Za>dyr8 z_xSH$`DPLfY!Yrzg7`s#0!tIrdgG?6s%ryvZSgWu+GC%{4MbffeO4-oh61&xB;qTL zZ7Hr==j?T>e(2{UYrT%{eu@8ZV8FK1HKOP*RQG>W43#^m^}T`wpO5FEcSzm`;ijA~ z`Ha%ACV^#cJd-r6J9g_Tw|aG7J==LMPse_nGVHvJC`j9~l$u!jp`++;XL{gatAuAk z3=tjb*w6`BH?<3q3v6?aeuVu&esPNK3St+o1&jwXjW#LNbO5O;1yb8$=d2o|v5*Q%P z@E7!|{h+q=zL{Fix29_njx_AL$y9a;x1wv4e1e}hgSi`mzl5p<1Rfen1N1iojTbl# zxKoRBnCPerm}9*XmVCP1qc(0BS^;*RqFr6_y#-FDLh?>v<5DiH))NEXd-^*1=%uAd z*GTt^ATpkV8shmEJ6X5B;Zx0$hU7XF&leYGIgqfjRup2-2Kt*A@owTXouvx|2wsV| zHk-Rn8g$E#P2UdZR9+EH&35Q;o@j^_up{2$oCq*=r%bp>cRS&O#^u>y*tZXkKXP8O zMfmj(>ivdmEBZ@f7KJi&?^0YJ?9IF3=8~6Tg1L#@l2~3B9~r#=>d8eC!}gp_Su1ru zQvH2Q7A$?flOe~L;j_O9v*OVJPOZj~2J!J(x12jMiBiMI8qzQKen!+PgCe{wtS^%$!Y{&+zi)!DB5gSUepeNxkb1IKcJ?Fdd( zj4|{XmdDy93rm;*_E3>>;;=w6`<(XMRN>X6aa4Xz9MPW z{R6MuoajOYd{G?HiiSIn?0&2G(Hfj{s@x&>o-H@)Zhe7zPQzjqXbRXAmz z`;}>>(x~{lA`RW1LRNk|knIC%EZ@|Ih?so|UQ9c-ja1CpO{jU^=xHLx=Puy8V|bF^ zZD#cybAmB(f0_W|x06tdUSkhje*dd18_QNu6umT-)gMQimU3?`f;!b`S}Ok}3ZD+&b&w~dKjnT01&zb0VX2GFNKhkq>5 z0ubuU3-`M-HdNeb6nyKQwRvf+(FV9B3V4o=_$W@@1-hQdj+hnRXx|Y z`=LwX3ltdUVpb|_4TvOi4{4bQc{gl~jf@B}hhv^FKd1C*^z#8@`k5XHx%52sylbP5 z8SlLve<|tb&**SYTOaQcdb<&fA=Xu|xw*Oz6m;8V3hRb7zRvm_Df0PrO6=bEc&~@| z-VZO@wxbqY?-J){8)PO-=jY)>Lws9Y*Dyh72ArG4NQar8iPXyNh(zg42AXsGsC&6j z0v!W=F-vH>H??S!&DN$bDF(FEQMTIsryYzs=0XJ#$K9r8f8}+WLiue|xsG$lHZb*t znAN!I0i+Fd%&BR1NwJ567GIBqP&rOC`BoIvabh}o2%N0gzqU3O<(KH+?M^(TfWweA z+!>7W&oXZ7B*k_obt4_7Zsp?9gmrxh72bfZC!HXV*RswA{Z@4?(#9RwRCRxQ=$J%w zlwqT=jiEz1${`N0!LqidJI(3S%4F1;bY*h!)6+q#@!F*4YMs3+hk(42T5?re2jo^Z zX00FZ9$|gNFFlO(!t54e(*xDDgPw%VH`)hm8{B&jbrZW zdB^lUox|S|*^Qd>0sDy>JX-y`OXeAQBJm8(`#=>3T+~#ZTV6#7hAM{dc3jQHC#$k= zlzho_$4*J7;sKZ>@5l(Dgm;;g-Q4QZ#-(>0W?`lVkKb0mirjz>AFx+-6rLQLEiCeM zFAu&8yov!X=0cR!5y855Y7}2fX$%?Nud}!`D|Uakg6~8h|3}RJ7Z4lx0X!@P9(Kgs zMO~sURRKS_D(AxXiMO_x?<@w{>G`**cf(t6`qh_rZTjK^5S&`FO`lS*@3}Y-iCh%9%}@&LrR6 z)eSP@D-Ix)yG_5DY>Rfb{^H(T>m4G?;(0St9@kZV9%#^vkSqGNDlX~3`|Mw@Fe4z? ztxto@P2iG9`C9NpTE!jA8S^4fXNQ6it4%n$kUeac;IFg)_UVN90C}4Ox)O~hNHG^L zt@1cN)1S(P^1HaP#ZEm;XZdd?jg)AqV91U$rI%Qcff1feMYx{*xu6%EfmSpmDvoc> zj-Ui6T5VJ{24;+v5N&HMEXY&vJr+7ni%$%N>>*QkEgyAK&_DR7eeK-JYwgET44SilNF2ORMrp4;)$*U_~+52N7)f(?= z-n#S7?6fs0leoKdh(ZMO>7?gf(JEsdtLTK#1ub4?PEie>D2YGtN*_Nij19LZ8gxwC z*ILa5tlP$u+Qn$5gH0X(b4n`O+mnZ8m={^%9<-4(NbTwv9ZMAg{vs3LzDA&lo~r3+ zl*k}g|6o|G|4u9?yH<}G0)FPE6t2@IT_b)a0+H8hXD;#Oz_o$_Rup3_2U%Q-1tL)^ zqUt}Ri3*_d)8^ItCt>H`aZryQ^@dlk#P^1*Fn`HUU^AEh+QlOb*BaW<3SXcoF|O;J z5PU5zzA^n7Pl*rbqp(p8Z9IaSuIFqfYw5{&E6T+@N0jgfe=I^M)OccDG&4_us$!c<+gVL9u#(=z`j(9oxJf@ zshitl1k)L*ly2^@=d==IM;}g@d!klS1ikptTyY6hGa{bv5K4|-hX)NzGn&cgh9Ii; zBVB2SY5@LVoRw$jM4FgxzV7a2b)rZHS>4ID?Vy2@D%a-3v1`>;Ydhz==BTCwX9-A;L8O}Z;~!4MAJ{7$53wM-vfgN) z73fn`rBv0$r8yJ|h>|$6mIdlVc9!~8L}q=x48lsGo*egE*;cXq?37u?q28r-4hB@M z>i0x_dAkQo(xZ^YVj*X=;6$uD2Mx<(7Q_#jkkupK*V`q*5!d#8EtM=qVZb31uoQGI zWj3B3n)_ba%WFBMx$vh)`@c#r$X^K{86$W6-32D`^=6WpL!g?2R1t5LKi|sG>1pWu z3oYeOn^&bT4Sk|xG$|EtK0DuP-bwd?2gfl#>|=^H_9h*4W1UERY(BkET24bZ>HYn& zEZ>u-&g)M;30~dO)-ZT`wHXoQ<)tASo{Q1?kAc3%#-kYiAqwBd?8bvn zFT*Vi{^}6^#z6i{r2MO5)HAP`^5(K}rTXDCR`Z6aw*QAQO9=}{(NA6QjivDvx_Z=qUT$G9?cu@o0 zUu|km$wh)X&05?+w~rHQE`G3(+5Yd(1&x-JD`AIJ{4~ITRWh|mIBKkxEHPQHrP%yiG-($($yON^h5)#YXPp`&k;6rSglnK$39vUz5FSK zl8}e|%obUH$a!1UgXNr2?fL_A8_KK&f(+-h7^7P4Qj>_X1Rr84?7%lak@Q~e>KhY$ zmaDP?pApu)Zb7}+1B_0|_8A!&|Mt|Wxvrgv(hF_jp^rOzX)oqeXoNA-I9FhJ(5k>K zu~%$ZUMzYO+U`L6BQ*!WJBP_Jz9Q{S>olozLN|PwWCbOKh#T3z&f{EKrEQ8w*2z+n zB{VH9&dBvnR?4%X*k!V0CM0KWT83y-Oip2Gkq>It0T+h{dB^Um4INxU2m1xaBX^c~ zw{pD34jd~BQsna0EBo3$zZ_S@^WG#tli~mlfGLotE(8IF*z8A3)JU4a=%bSvJVw1D zdC~ybi9Tq_;q0D{#f)M9=}L4XgC~In!n|Cnd`*qR!PXp658+4>EK6Aw-WHi&(SlLc z>I#F_c#y$^^minhiqpF27p^%xl}e*)Mi5@*316C?L)Mls!_fX*S`1O&6Tj&ckM|~G zS0@T@?6@X^Eo`mLi{(2{MrjQY zXtV_r?kaEromhM&^e2$>!p}+A#+7Fq zNJ$0yYgFD%@LC%;aTpey+qJ5>9ZfViRlxI3j~Zn5#}C?)wxE8Bh#E?x_!Y7R^9pY%6YQ_? zcJTl;w1Z-O>-J$oRYveDTXJ6)>vQ(`f*7;=c1PxA?ntm?%?+HMPR`PiO74|zw?5fA z(b|6S`WPY39OrDbWbmS^C*}>475iP1>(&#d0kbwSYY$kdMt+;uh<{$4)Bf1?@N7Y^uG$WB#eB& zn{V~q{shl3L;2xg#WhUF5SIW{S}jq`YfJMNbYcWmA|wrZP`n+N8dGpJYu54WQT7C%MJ({cL3N+L4-v2!?x_xQ0h|K7q{Bp-@g z9WbO1xy$>{>B-+XXT5Qr2WRtY6C{H+@PW^2y12WdV8&4P`C7QEi}ZMr z6TPBd6WY8ohX!*dziu&KpJ2A7ByvwCu(F7lt;_8gW?wbGyo%Fh4-3Tw^kTyHbxpLC zWUhrdX;H)lK2lRV4Z+Po2vDimWxZVN2E;+kG~4|Rq`iO|1HSCks$Ju`mnx;L%m0Ac3%bA0Y056IB4H%DI z1M$zon$k{EOK|`QFs^x#5^f!~oz%avGhe6jOrf>KG9!bFM$9vC9cRD+%Po)6!jEo6P=TOQYcqDUoffW>b zb+u(U_3blgBrlfLA$I6ly8r>=*rDvP+tAE|4~P$9=gWr&@ozlw-NUU+4bUc`nY8bt zB@~E-2p0>E79vKez7G^Xrgl=5`1C$Rr&NBZM~8Vj4rhn? z%Pl$<9J{7A(K|{F#G~MFihk9jPo~TiU+LEq7~w zx!JU-AvG$#%~a}_X0ql@H^J#@%W*^fb$5QoS>LGfVh@^RIN>sy3*Cp;QY5NsxLO=h zp(VZ^roV8fP*v7%%Bojm7JIhD2PI1m5PW0W>U?;31&!=@`$!fZ%Q6!94n=@nZNI47 zp=h6M5s&ZxzBO@K(w*7Qge?N1k8JiBK_)amZuh0RFlcCCF7l#Qn-h$V2E>Laf_v|R z25pl~KFD}Cl_5`GTC-`b*aBlW3BZaz6_q_2V1&LtBj{J3QVF`FN2+{6gz1XyYbQ`d z@Br&hN2Uw1rHY#}&Q+rUb_^_&loUnc)ngP#$k#xCz5(EQgV;n&!v+}cQyesq(J-7% zvQI7ZakKiH`V%d6uWkb@4LU71yYUx0Cc;8(J>(~BMI%cm@%NIz7G{LRfxNBWE$@Mf z9hHiBhPpPkR_71AV-QW&5^B=w4?KnN2nY*y<7|VX^f6bEFgw6Qt}mUPEmy^fKt=$i z8rTL4=s8%l-nO1bP|cYsioa^0Qk~NVp<{_(vzTS+SgRNFkG-eApr8MjX>`5SymGq# ztH#%Cp=~)cygn%uHW-yU=_u6*KawN`$F7FReunRI2@QGdDD3s>JOgwUgAAkR_fo%! zpxGo!O8IYm*5P!-)w#r98dB+8q8H#5^?t3rmh$6+RQM+9N1AjteQQ@uE-B%?n{x8 zQ`)rxxdS!+b0s(rXKaMdfg#PIUjTkV)wxmmX}6ihmU@7ma$_il?&TTlV5P;IWkrbz zvgl?h#;g$ORxi3Q{Opdena`_pHfFK|6c*o7Fo8MDtfj zTlP*;KUAP;pw3H&;T^QSSLWMG$oo$muX97wox~jvB8qa;{tsbq9nf_Dwhd!}K`0>- z5>nD#5)+Z`?(P;4k=hiL?i!7NNP{#eFce3Q?v@yx8#Ng7`Fh3gciq?hJn#Dle{2K3 zi_gwD&f`3eD5MMIkr_`ICb!sy>#eofqt|;jN^g?6#k!GKIdF%5w*$q>)>MHjPC{6j z1P!-_uU>H+clN(s7Co~8j}TqiLcKPG0}fjid3&%FYFL5igQ`$~Q4e|wW34ZwyvHfp z!I2CS96jt-0x0#K+0IQrni6l>)%uG*edPxGWZU<;X;_$f&ei)KLaW(W4DztXbI%1T z;g`FHd&669GZybbxwm+!4-5-HRgX+8Er`Hw829(9cUHo^=()CEh0ZL!mq5zBRYv_^ zpc!DJUp<#b&6m&-PW4gqfzR~>*l;AwL}(w=1J5h|yqh()+mwS@k$?@zkfmfPhAKKBxp|u}P}5 z4icE@k}6f4n3z-{)Iy&{JGM{_vCr;a&hzX00Z~P2yd|Pzkj~}EhPmASQtiracNf69 zuMsPi0&@04)oz>2ew>pXE9`J&rO`@HW|ZP}?kW_!a`~xfjpWHR2_GhS+OSLzXp z)AhWeMd2+BgjWA8%C=4)__h$NiwnXO^?qSa>a)&J`Bf1IEEKmfbR^@Cj-NP@bU;RgLxyo^G2NCg@u7$ z_)J%8Et3EU3^$CA3&j6Y@t9NlKJuwa8mA6zE?On%qcoKypxDS4T`ovi_}t`e=j5qsl=e)c3j#}nEoSed$uE2GM zQd>k}HY;CURo7>bXS2<&?!2Ky>$<~OX(RPr_Q~(*VWJ!)WN1-j(dzpHkCBN8atIZ;Zi}4CIa1tc|$#o={}uM&pcD@1X7f8g_o4(Z4ot|Cg^}=}7|B$J>1G0~V2@sdVRCEP5r@c(aQF zq2Fqgj$BZ%zI)ROgbd)l8`m!qM)$>KJQvD1HElyklg*^Od9HM%tHc7DVJmFLM6elv zEm;bRwOWTNNNCWNELe&E34SX{eiQv39}D>AoC}I-&{Y!Wn~_r6d%vl2PZ7U$zxy3( z{4u^|ck>XT#o%c^MPu;S+WSS__V}_}L>-x{xdUbf@7y_48`{XxLDOHxa;KI{Naazk zZ<2O6BR}c4nM_nQ)YmjaFBQ#TnsB2(*b8V(MXB7jF~&vL@r0qiw$Pb_9l`CX+qsDI z^hU?7lI9q}$Y+p7I_nM#1WpJgzmG4(|*(tQFzzVW@Kwf zH$57F9@G70*~d&Y!mpVXIRLu^pE%AXdUNA(R>S)I-Ym_xWhBjF+fU9s4H`9u)?8_+ zSW`JW=Lzb;*sk>Xo;TC?QW}^`{Sdo=DPl*@ciII542IE6U|8aQ{OBsazIgL%3``m% zWGh`Z3W#Wiy;-D2c;BsO{LJ~U@$%#^Jfs6gcL-*Hmt8JqUW#tm%1M&%cl+ z;~2j={hB*Dob`||ZnmWD+HbHDBOw%iVM&3Fm^&U-m|!BELF43C31*!WwQ`=!5X11!J5ZmP zRtF{VE<{kFp4CaUzJ*^AfU*fp-&}=AyTsOv-H1gk^-jL9h!!@NlIGI83c%Iy=MAE@ zm@eaFC3q7}R0xCcvXTq|N6x3#)@nVEZe9d^k4GPGf42GQvq?KTv*wXJx#lrUm`koM zWsFe*ZLyq=f!2k|C`Dj4W_OKSv>%pkjB76FWgFewUrVKW%J77&7O!2HqzP^w5LpZA zrOrhVT?;R{WOr!PIQ3et8Xv^;%?lrcda?r*B-)3*CrwCKY${f@1NK&9dEbdR0T=fW zbRsY({F3E6p-y^0O!j)H97v2n&^_GKaR15B+kbv5u?z5BJC4FvT>1oA6m{S z=N`F_HrBaRom!IA;>Ylj{)~tA3&lQ9zpvPBYkB+ghRlXMf&16LxCEhqn4kx@%e`r5 zk@7Erb+=wp7bI+J{cJt03jD&iOd~eh-|iUmby_Vo !7wI-c4`sndDn7*!@XfO%0 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 literal 0 HcmV?d00001 From 4f026e625391eba91834dd24d3fe3dce97ad0ab2 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 4 Oct 2024 15:36:22 -0300 Subject: [PATCH 26/37] Fixed BOM --- API.Benchmark/ArchiveServiceBenchmark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index 31f3088e82..589433360a 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Abstractions; using API.Entities.Enums; From 00ee60b485893fa364eced0477d8f791c16ed8da Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 10 Oct 2024 17:57:33 -0300 Subject: [PATCH 27/37] Requested changes. Added Per Image Lock on conversion, to prevent multiple threads from processing the same image at the same time. --- API/Entities/Enums/EncodeFormat.cs | 6 +- API/Extensions/HttpExtensions.cs | 22 +- API/Services/ImageService.cs | 84 +- API/Services/ImageService.cs.bak | 994 ++++++++++++++++++ .../ImageMagick/ImageMagickImage.cs | 13 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 22 +- 6 files changed, 1096 insertions(+), 45 deletions(-) create mode 100644 API/Services/ImageService.cs.bak diff --git a/API/Entities/Enums/EncodeFormat.cs b/API/Entities/Enums/EncodeFormat.cs index f42ba4586f..0d61409276 100644 --- a/API/Entities/Enums/EncodeFormat.cs +++ b/API/Entities/Enums/EncodeFormat.cs @@ -1,8 +1,8 @@ -using System.ComponentModel; +using System.ComponentModel; namespace API.Entities.Enums; -public enum EncodeFormat +public enum EncodeFormat { [Description("PNG")] PNG = 0, @@ -10,7 +10,7 @@ public enum EncodeFormat WEBP = 1, [Description("AVIF")] AVIF = 2, - //Internal Use + /// Internal Use [Description("JPEG")] JPEG = 3 } diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 32d82f9ffb..5e6f76d657 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -80,28 +80,30 @@ public static List SupportedImageTypesFromRequest(this HttpRequest reque List supportedExtensions = new List(); - //Add default extensions supported by all browsers. + // Add default extensions supported by all browsers. supportedExtensions.AddRange(Parser.UniversalFileImageExtensionArray); - //Browser add specific image mime types, when the image type is not a global standard, browser specify the specific image type in the accept header. - //Let's reuse that to identify the additional image types supported by the browser. - foreach (string v in split) + + // Browser add specific image mime types, when the image type is not a global standard, browser specify the specific image type in the accept header. + // Let's reuse that to identify the additional image types supported by the browser. + foreach (var v in split) { if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) { - string mimeimagepart = v.Substring(6).ToLowerInvariant(); - if (mimeimagepart.StartsWith("*")) continue; - if (Parser.NonUniversalSupportedMimeMappings.ContainsKey(mimeimagepart)) + var mimeImagePart = v.Substring(6).ToLowerInvariant(); + if (mimeImagePart.StartsWith("*")) continue; + if (Parser.NonUniversalSupportedMimeMappings.ContainsKey(mimeImagePart)) { - Parser.NonUniversalSupportedMimeMappings[mimeimagepart].ForEach(x => AddExtension(supportedExtensions, x)); + Parser.NonUniversalSupportedMimeMappings[mimeImagePart].ForEach(x => AddExtension(supportedExtensions, x)); } - else if (mimeimagepart == "svg+xml") + else if (mimeImagePart == "svg+xml") { AddExtension(supportedExtensions, "svg"); } else - AddExtension(supportedExtensions, mimeimagepart); + AddExtension(supportedExtensions, mimeImagePart); } } + return supportedExtensions; } } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 194053b9ea..0f8991b5ae 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -1,11 +1,15 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Numerics; +using System.Security.Cryptography; +using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using API.Constants; using API.DTOs; @@ -155,19 +159,13 @@ public interface IImageService public class ImageService : IImageService { public const string Name = "ImageService"; - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IEasyCachingProviderFactory _cacheFactory; - private readonly IImageFactory _imageFactory; 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+"; - private const double WhiteThreshold = 0.95; // Colors with lightness above this are considered too close to white private const double BlackThreshold = 0.25; // Colors with lightness below this are considered too close to black - /// /// Width of the Thumbnail generation /// @@ -181,6 +179,10 @@ public class ImageService : IImageService ///
  • public const int LibraryThumbnailWidth = 32; + private readonly ILogger _logger; + private readonly IDirectoryService _directoryService; + private readonly IEasyCachingProviderFactory _cacheFactory; + private readonly IImageFactory _imageFactory; private static readonly string[] ValidIconRelations = { "icon", @@ -197,6 +199,21 @@ public class ImageService : IImageService ["https://app.plex.tv"] = "https://plex.tv" }; + + + private static NamedMonitor _lock = new NamedMonitor(); + /// + /// Represents a named monitor that provides thread-safe access to objects based on their names. + /// + class NamedMonitor + { + readonly ConcurrentDictionary _dictionary = new ConcurrentDictionary(); + + public object this[string name] => _dictionary.GetOrAdd(name, _ => new object()); + } + + + /// /// Initializes a new instance of the class. /// @@ -229,7 +246,7 @@ public void ExtractImages(string? fileFilePath, string targetDirectory, int file Tasks.Scanner.Parser.Parser.ImageFileExtensions); } } - + /// /// Creates a thumbnail image from the specified image with the given width and height. /// If the image aspect ratio is significantly different from the target aspect ratio, it will be context aware cropped to fit. @@ -332,12 +349,10 @@ public string WriteCoverThumbnail(Stream stream, string fileName, string outputD try { thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); - return filename; - } - catch (Exception) - { - return string.Empty; //IDK? } + catch (Exception) {/* Swallow exception */} + + return filename; } /// public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) @@ -351,6 +366,7 @@ public string WriteCoverThumbnail(string sourceFile, string fileName, string out _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + return filename; } /// @@ -361,6 +377,7 @@ public async Task ConvertToEncodingFormat(string filePath, string output var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); using var sourceImage = _imageFactory.Create(filePath); await sourceImage.SaveAsync(outputFile, encodeFormat, quality).ConfigureAwait(false); + return outputFile; } @@ -369,9 +386,7 @@ public Task IsImage(string filePath) { try { - var result= _imageFactory.GetDimensions(filePath); - if (result!=null) - return Task.FromResult(true); + return Task.FromResult(_imageFactory.GetDimensions(filePath) != null); } catch (Exception) { @@ -521,7 +536,7 @@ public async Task DownloadPublisherImageAsync(string publisherName, Enco return (null, null); } - + private static bool IsColorCloseToWhiteOrBlack(Vector3 color) { var (_, _, lightness) = RgbToHsl(color); @@ -690,26 +705,41 @@ public ColorScape CalculateColorScape(string sourceFile) } private bool CheckDirectSupport(string filename, List supportedImageFormats) { - if (supportedImageFormats == null) - return false; + if (supportedImageFormats == null) return false; + string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); return supportedImageFormats.Contains(ext); } + + /// public string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99) { - if (CheckDirectSupport(filename, supportedImageFormats)) - return filename; + if (CheckDirectSupport(filename, supportedImageFormats)) return filename; + Match m = Regex.Match(Path.GetExtension(filename), Parser.NonUniversalFileImageExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); - if (!m.Success) - return filename; - var sw = Stopwatch.StartNew(); + if (!m.Success) return filename; + string destination = Path.ChangeExtension(filename, format.GetExtension().Substring(1)); - using var sourceImage = _imageFactory.Create(filename); - sourceImage.Save(destination, format, quality); - File.Delete(filename); - _logger.LogDebug("Image converted from '{Extension}' to '{format.GetExtension()}' in {ElapsedMilliseconds} milliseconds", Path.GetExtension(filename), sw.ElapsedMilliseconds); + // Adding a lock per destination, sometimes web ui triggers same image loading at the ~ same time. + // So, if other thread is already processing the image, we should wait for it to finish, then the File.Exists(destination) will early exit. + // This exists, to prevent multiple threads from processing the same image at the same time. + lock (_lock[destination]) + { + if (File.Exists(destination)) + { + // Destination already exist, the conversion was already made. + return destination; + } + using var sourceImage = _imageFactory.Create(filename); + sourceImage.Save(destination, format, quality); + try + { + File.Delete(filename); + } + catch (Exception) { /* Swallow Exception */ } + } return destination; } diff --git a/API/Services/ImageService.cs.bak b/API/Services/ImageService.cs.bak new file mode 100644 index 0000000000..4b512a79a1 --- /dev/null +++ b/API/Services/ImageService.cs.bak @@ -0,0 +1,994 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using API.Constants; +using API.DTOs; +using API.Entities.Enums; +using API.Entities.Interfaces; +using API.Extensions; +using API.Services.ImageServices; +using API.Services.Tasks.Scanner.Parser; +using EasyCaching.Core; +using Flurl; +using Flurl.Http; +using HtmlAgilityPack; +using Kavita.Common; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.Extensions.Logging; + + +namespace API.Services; +/// +/// Interface for the ImageService. +/// +public interface IImageService +{ + /// + /// Extracts images from a file and copies them to the target directory. + /// + /// The file path of the source file. + /// The target directory to copy the images to. + /// The number of files to extract. Default is 1. + void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); + + /// + /// Gets the cover image from the specified path and saves it to the output directory. + /// + /// The path of the image file. + /// The name of the output file. + /// The output directory to save the image. + /// The encoding format to convert and save the image. + /// The size of the cover image. + /// The quality of the image. Default is 100. + /// The file name with extension of the saved image. + string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100); + + /// + /// Creates a thumbnail version of a base64 encoded image. + /// + /// The base64 encoded image. + /// The name of the output file. + /// The encoding format to convert and save the image. + /// The width of the thumbnail. Default is 320. + /// The quality of the image. Default is 100. + /// The file name with extension of the saved thumbnail image. + string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, int quality = 100); + + /// + /// Creates a thumbnail out of a memory stream and saves to with the passed + /// fileName and the appropriate 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 the passed encoding + /// The size of the cover image. + /// The quality of the image. Default is 100. + /// File name with extension of the file. This will always write to + string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); + + /// + /// Writes out a thumbnail image from a file path input and saves to with the passed + /// + /// The path of the source image file. + /// filename to save as without extension + /// Where to output the file, defaults to covers directory + /// Export the file as the passed encoding + /// The size of the cover image. + /// The quality of the image. Default is 100. + /// File name with extension of the file. This will always write to + string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); + + /// + /// Converts the specified image file to the specified encoding format and saves it to the output path. + /// + /// The full path of the image file to convert. + /// The path to save the converted image file. + /// The encoding format to convert the image. + /// The quality of the image. Default is 100. + /// The file path of the converted image file. + Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100); + + /// + /// Checks if the specified file is an image. + /// + /// The path of the file to check. + /// True if the file is an image; otherwise, false. + Task IsImage(string filePath); + + /// + /// Downloads the favicon from the specified URL and saves it to the output directory. + /// + /// The URL of the favicon. + /// The encoding format to convert and save the favicon. + /// The quality of the favicon. Default is 100. + /// The file name with extension of the saved favicon. + Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100); + + /// + /// Downloads the publisher image for the specified publisher name and saves it to the output directory. + /// + /// The name of the publisher. + /// The encoding format to convert and save the publisher image. + /// The quality of the publisher image. Default is 100. + /// The file name with extension of the saved publisher image. + Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100); + + /// + /// Updates the color scape of an entity with a cover image. + /// + /// The entity with a cover image. + void UpdateColorScape(IHasCoverImage entity); + + /// + /// Replaces the file format of an image with the specified supported image formats by the browser, if needed, otherwise original file will be served. + /// + /// The name of the image file. + /// The list of supported image formats by the browser. + /// The encoding format to convert the image. Default is JPG + /// The quality of the image. Default is 99. + /// The file name with extension of the replaced image file. + string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99); + + /// + /// Creates a merged image from a list of cover images and saves it to the specified destination. + /// + /// The list of cover images. + /// The size of the merged image. + /// The destination path to save the merged image. + /// The encoding format to convert and save the merged image. Default is PNG. + /// The quality of the merged image. Default is 100. + void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100); + + /// + /// The image factory used to create and manipulate images. + /// + IImageFactory ImageFactory { get; } +} + +public class ImageService : IImageService +{ + public const string Name = "ImageService"; + 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+"; + private const double WhiteThreshold = 0.95; // Colors with lightness above this are considered too close to white + private const double BlackThreshold = 0.25; // Colors with lightness below this are considered too close to black + + /// + /// Width of the Thumbnail generation + /// + private const int ThumbnailWidth = 320; + /// + /// Height of the Thumbnail generation + /// + private const int ThumbnailHeight = 455; + /// + /// Width of a cover for Library + /// + public const int LibraryThumbnailWidth = 32; + + private readonly ILogger _logger; + private readonly IDirectoryService _directoryService; + private readonly IEasyCachingProviderFactory _cacheFactory; + private readonly IImageFactory _imageFactory; + + private static readonly string[] ValidIconRelations = { + "icon", + "apple-touch-icon", + "apple-touch-icon-precomposed", + "apple-touch-icon icon-precomposed" // ComicVine has it combined + }; + + /// + /// A mapping of urls that need to get the icon from another url, due to strangeness (like app.plex.tv loading a black icon) + /// + private static readonly IDictionary FaviconUrlMapper = new Dictionary + { + ["https://app.plex.tv"] = "https://plex.tv" + }; + + + + private static NamedMonitor _lock = new NamedMonitor(); + /// + /// Represents a named monitor that provides thread-safe access to objects based on their names. + /// + class NamedMonitor + { + readonly ConcurrentDictionary _dictionary = new ConcurrentDictionary(); + + public object this[string name] => _dictionary.GetOrAdd(name, _ => new object()); + } + + + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The directory service. + /// The cache factory. + /// The image factory. + public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory, IImageFactory imageFactory) + { + _logger = logger; + _directoryService = directoryService; + _cacheFactory = cacheFactory; + _imageFactory = imageFactory; + } + + public IImageFactory ImageFactory => _imageFactory; + + /// + 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); + } + } + + /// + /// Creates a thumbnail image from the specified image with the given width and height. + /// If the image aspect ratio is significantly different from the target aspect ratio, it will be context aware cropped to fit. + /// + /// The image to create a thumbnail from. + /// The width of the thumbnail. + /// The height of the thumbnail. + /// The thumbnail image. + public static IImage Thumbnail(IImage image, int width, int height) + { + try + { + if (WillScaleWell(image, width, height) || IsLikelyWideImage(image.Width, image.Height)) + { + image.Thumbnail(width, height); + return image; + } + } + catch (Exception) + { + /* Swallow */ + } + var crop = SmartCrop.Crop(image, new SmartCrop.SmartCropOptions { Width = width, Height = height }); + if (crop.TopCrop.Width != width && crop.TopCrop.Height != height) + { + image.Crop(crop.TopCrop.X, crop.TopCrop.Y, crop.TopCrop.Width, crop.TopCrop.Height); + } + image.Thumbnail(width, height); + return image; + } + + public static bool WillScaleWell(IImage sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) + { + // Calculate the aspect ratios + var sourceAspectRatio = (double) sourceImage.Width / sourceImage.Height; + var targetAspectRatio = (double) targetWidth / targetHeight; + + // Compare aspect ratios + if (Math.Abs(sourceAspectRatio - targetAspectRatio) > tolerance) + { + return false; // Aspect ratios differ significantly + } + + // Calculate scaling factors + var widthScaleFactor = (double) targetWidth / sourceImage.Width; + var heightScaleFactor = (double) targetHeight / sourceImage.Height; + + // Check resolution quality (example thresholds) + if (widthScaleFactor > 2.0 || heightScaleFactor > 2.0) + { + return false; // Scaling factor too large + } + + return true; // Image will scale well + } + + private static bool IsLikelyWideImage(int width, int height) + { + var aspectRatio = (double) width / height; + return aspectRatio > 1.25; + } + + + /// + public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100) + { + if (string.IsNullOrEmpty(path)) return string.Empty; + + try + { + var (width, height) = size.GetDimensions(); + using var thumbnail = Thumbnail(_imageFactory.Create(path), width, height); + var filename = fileName + encodeFormat.GetExtension(); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + 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; + } + + /// + public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) + { + var (targetWidth, targetHeight) = size.GetDimensions(); + if (stream.CanSeek) stream.Position = 0; + + using var thumbnail = Thumbnail(_imageFactory.Create(stream), targetWidth, targetHeight); + var filename = fileName + encodeFormat.GetExtension(); + _directoryService.ExistOrCreate(outputDirectory); + + try + { + _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + } catch (Exception) {/* Swallow exception */} + + try + { + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + } + catch (Exception) {/* Swallow exception */} + + return filename; + } + /// + public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) + { + var (width, height) = size.GetDimensions(); + using var thumbnail = Thumbnail(_imageFactory.Create(sourceFile), width, height); + var filename = fileName + encodeFormat.GetExtension(); + _directoryService.ExistOrCreate(outputDirectory); + try + { + _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + } catch (Exception) {/* Swallow exception */} + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + + return filename; + } + /// + public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100) + { + var file = _directoryService.FileSystem.FileInfo.New(filePath); + var fileName = file.Name.Replace(file.Extension, string.Empty); + var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); + using var sourceImage = _imageFactory.Create(filePath); + await sourceImage.SaveAsync(outputFile, encodeFormat, quality).ConfigureAwait(false); + + return outputFile; + } + + /// + public Task IsImage(string filePath) + { + try + { + return Task.FromResult(_imageFactory.GetDimensions(filePath) != null); + } + catch (Exception) + { + /* Swallow Exception */ + } + + return Task.FromResult(false); + } + /// + public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100) + { + // Parse the URL to get the domain (including subdomain) + var uri = new Uri(url); + var domain = uri.Host.Replace(Environment.NewLine, string.Empty); + var baseUrl = uri.Scheme + "://" + uri.Host; + + + var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon); + var res = await provider.GetAsync(baseUrl); + if (res.HasValue) + { + _logger.LogInformation("Kavita has already tried to fetch from {BaseUrl} and failed. Skipping duplicate check", baseUrl); + throw new KavitaException($"Kavita has already tried to fetch from {baseUrl} and failed. Skipping duplicate check"); + } + + await provider.SetAsync(baseUrl, string.Empty, TimeSpan.FromDays(10)); + if (FaviconUrlMapper.TryGetValue(baseUrl, out var value)) + { + url = value; + } + + var correctSizeLink = string.Empty; + + try + { + var htmlContent = url.GetStringAsync().Result; + var htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(htmlContent); + var pngLinks = htmlDocument.DocumentNode.Descendants("link") + .Where(link => ValidIconRelations.Contains(link.GetAttributeValue("rel", string.Empty))) + .Select(link => link.GetAttributeValue("href", string.Empty)) + .Where(href => href.Split("?")[0].EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + + correctSizeLink = (pngLinks?.Find(pngLink => pngLink.Contains("32")) ?? pngLinks?.FirstOrDefault()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading favicon.png for {Domain}, will try fallback methods", domain); + } + + try + { + if (string.IsNullOrEmpty(correctSizeLink)) + { + correctSizeLink = await FallbackToKavitaReaderFavicon(baseUrl); + } + if (string.IsNullOrEmpty(correctSizeLink)) + { + throw new KavitaException($"Could not grab favicon from {baseUrl}"); + } + + var finalUrl = correctSizeLink; + + // If starts with //, it's coming usually from an offsite cdn + if (correctSizeLink.StartsWith("//")) + { + finalUrl = "https:" + correctSizeLink; + } + else if (!correctSizeLink.StartsWith(uri.Scheme)) + { + finalUrl = Url.Combine(baseUrl, correctSizeLink); + } + + _logger.LogTrace("Fetching favicon from {Url}", finalUrl); + // Download the favicon.ico file using Flurl + var faviconStream = await finalUrl + .AllowHttpStatus("2xx,304") + .GetStreamAsync(); + + // Create the destination file path + using var image = _imageFactory.Create(faviconStream); + var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); + await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); + _logger.LogDebug("Favicon for {Domain} downloaded and saved successfully", domain); + return filename; + } catch (Exception ex) + { + _logger.LogError(ex, "Error downloading favicon for {Domain}", domain); + throw; + } + } + /// + public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100) + { + try + { + var publisherLink = await FallbackToKavitaReaderPublisher(publisherName); + if (string.IsNullOrEmpty(publisherLink)) + { + throw new KavitaException($"Could not grab publisher image for {publisherName}"); + } + + var finalUrl = publisherLink; + + _logger.LogTrace("Fetching publisher image from {Url}", finalUrl); + // Download the favicon.ico file using Flurl + var publisherStream = await finalUrl + .AllowHttpStatus("2xx,304") + .GetStreamAsync(); + + // Create the destination file path + using var image = _imageFactory.Create(publisherStream); + var filename = GetPublisherFormat(publisherName, encodeFormat); + await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); + _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName); + return filename; + } catch (Exception ex) + { + _logger.LogError(ex, "Error downloading image for {PublisherName}", publisherName); + throw; + } + } + + private (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) + { + + + var rgbPixels = _imageFactory.GetRgbPixelsPercentage(imagePath, 10); + + // Perform k-means clustering + var clusters = KMeansClustering(rgbPixels, 4); + + var sorted = SortByVibrancy(clusters); + + // Ensure white and black are not selected as primary/secondary colors + sorted = sorted.Where(c => !IsCloseToWhiteOrBlack(c)).ToList(); + + if (sorted.Count >= 2) + { + return (sorted[0], sorted[1]); + } + if (sorted.Count == 1) + { + return (sorted[0], null); + } + + return (null, null); + } + + private static bool IsColorCloseToWhiteOrBlack(Vector3 color) + { + var (_, _, lightness) = RgbToHsl(color); + return lightness is > WhiteThreshold or < BlackThreshold; + } + + private static List KMeansClustering(List points, int k, int maxIterations = 100) + { + var random = new Random(); + var centroids = points.OrderBy(x => random.Next()).Take(k).ToList(); + + for (var i = 0; i < maxIterations; i++) + { + var clusters = new List[k]; + for (var j = 0; j < k; j++) + { + clusters[j] = []; + } + + foreach (var point in points) + { + var nearestCentroidIndex = centroids + .Select((centroid, index) => new { Index = index, Distance = Vector3.DistanceSquared(centroid, point) }) + .OrderBy(x => x.Distance) + .First().Index; + clusters[nearestCentroidIndex].Add(point); + } + + var newCentroids = clusters.Select(cluster => + cluster.Count != 0 ? new Vector3( + cluster.Average(p => p.X), + cluster.Average(p => p.Y), + cluster.Average(p => p.Z) + ) : Vector3.Zero + ).ToList(); + + if (centroids.SequenceEqual(newCentroids)) + break; + + centroids = newCentroids; + } + + return centroids; + } + + public static List SortByBrightness(List colors) + { + return colors.OrderBy(c => 0.299 * c.X + 0.587 * c.Y + 0.114 * c.Z).ToList(); + } + + private static List SortByVibrancy(List colors) + { + return colors.OrderByDescending(c => + { + var max = Math.Max(c.X, Math.Max(c.Y, c.Z)); + var min = Math.Min(c.X, Math.Min(c.Y, c.Z)); + return (max - min) / max; + }).ToList(); + } + + private static bool IsCloseToWhiteOrBlack(Vector3 color) + { + var threshold = 30; + return (color.X > 255 - threshold && color.Y > 255 - threshold && color.Z > 255 - threshold) || + (color.X < threshold && color.Y < threshold && color.Z < threshold); + } + + private static string RgbToHex(Vector3 color) + { + return $"#{(int)color.X:X2}{(int)color.Y:X2}{(int)color.Z:X2}"; + } + + private static Vector3 GetComplementaryColor(Vector3 color) + { + // Convert RGB to HSL + var (h, s, l) = RgbToHsl(color); + + // Rotate hue by 180 degrees + h = (h + 180) % 360; + + // Convert back to RGB + return HslToRgb(h, s, l); + } + + private static (double H, double S, double L) RgbToHsl(Vector3 rgb) + { + double r = rgb.X / 255; + double g = rgb.Y / 255; + double b = rgb.Z / 255; + + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + var diff = max - min; + + double h = 0; + double s = 0; + var l = (max + min) / 2; + + if (Math.Abs(diff) > 0.00001) + { + s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min); + + if (max == r) + h = (g - b) / diff + (g < b ? 6 : 0); + else if (max == g) + h = (b - r) / diff + 2; + else if (max == b) + h = (r - g) / diff + 4; + + h *= 60; + } + + return (h, s, l); + } + + private static Vector3 HslToRgb(double h, double s, double l) + { + double r, g, b; + + if (Math.Abs(s) < 0.00001) + { + r = g = b = l; + } + else + { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = HueToRgb(p, q, h + 120); + g = HueToRgb(p, q, h); + b = HueToRgb(p, q, h - 120); + } + + return new Vector3((float)(r * 255), (float)(g * 255), (float)(b * 255)); + } + + private static double HueToRgb(double p, double q, double t) + { + if (t < 0) t += 360; + if (t > 360) t -= 360; + return t switch + { + < 60 => p + (q - p) * t / 60, + < 180 => q, + < 240 => p + (q - p) * (240 - t) / 60, + _ => p + }; + } + + /// + /// Generates the Primary and Secondary colors from a file + /// + /// This may use a second most common color or a complementary color. It's up to implemenation to choose what's best + /// + /// + public ColorScape CalculateColorScape(string sourceFile) + { + if (!File.Exists(sourceFile)) return new ColorScape() {Primary = null, Secondary = null}; + + var colors = GetPrimarySecondaryColors(sourceFile); + + return new ColorScape() + { + Primary = colors.Item1 == null ? null : RgbToHex(colors.Item1.Value), + Secondary = colors.Item2 == null ? null : RgbToHex(colors.Item2.Value) + }; + } + private bool CheckDirectSupport(string filename, List supportedImageFormats) + { + if (supportedImageFormats == null) return false; + + string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); + return supportedImageFormats.Contains(ext); + } + + + /// + public string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99) + { + if (CheckDirectSupport(filename, supportedImageFormats)) return filename; + + Match m = Regex.Match(Path.GetExtension(filename), Parser.NonUniversalFileImageExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); + if (!m.Success) return filename; + + string destination = Path.ChangeExtension(filename, format.GetExtension().Substring(1)); + + // Adding a lock per destination, sometimes web ui triggers same image loading at the ~ same time. + // So, if other thread is already processing the image, we should wait for it to finish, then the File.Exists(destination) will early exit. + // This exists, to prevent multiple threads from processing the same image at the same time. + lock (_lock[destination]) + { + if (File.Exists(destination)) + { + // Destination already exist, the conversion was already made. + return destination; + } + using var sourceImage = _imageFactory.Create(filename); + sourceImage.Save(destination, format, quality); + try + { + File.Delete(filename); + } + catch (Exception) { /* Swallow Exception */ } + } + return destination; + } + + private static async Task FallbackToKavitaReaderFavicon(string baseUrl) + { + var correctSizeLink = string.Empty; + var allOverrides = await "https://www.kavitareader.com/assets/favicons/urls.txt".GetStringAsync(); + if (!string.IsNullOrEmpty(allOverrides)) + { + var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty); + var externalFile = allOverrides + .Split("\n") + .FirstOrDefault(url => + cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) || + cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty) + )); + + if (string.IsNullOrEmpty(externalFile)) + { + throw new KavitaException($"Could not grab favicon from {baseUrl}"); + } + + correctSizeLink = "https://www.kavitareader.com/assets/favicons/" + externalFile; + } + + return correctSizeLink; + } + + private static async Task FallbackToKavitaReaderPublisher(string publisherName) + { + var externalLink = string.Empty; + var allOverrides = await "https://www.kavitareader.com/assets/publishers/publishers.txt".GetStringAsync(); + if (!string.IsNullOrEmpty(allOverrides)) + { + var externalFile = allOverrides + .Split("\n") + .Select(publisherLine => + { + var tokens = publisherLine.Split("|"); + if (tokens.Length != 2) return null; + var aliases = tokens[0]; + // Multiple publisher aliases are separated by # + if (aliases.Split("#").Any(name => name.ToLowerInvariant().Trim().Equals(publisherName.ToLowerInvariant().Trim()))) + { + return tokens[1]; + } + return null; + }) + .FirstOrDefault(url => !string.IsNullOrEmpty(url)); + + if (string.IsNullOrEmpty(externalFile)) + { + throw new KavitaException($"Could not grab publisher image for {publisherName}"); + } + + externalLink = "https://www.kavitareader.com/assets/publishers/" + externalFile; + } + + return externalLink; + } + + /// + public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth, int quality = 100) + { + try + { + + using var thumbnail = _imageFactory.CreateFromBase64(encodedImage); + int thumbnailHeight = (int)(thumbnail.Height * ((double)thumbnailWidth / thumbnail.Width)); + thumbnail.Thumbnail(thumbnailWidth, thumbnailHeight); + fileName += encodeFormat.GetExtension(); + thumbnail.Save(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName), encodeFormat, quality); + 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 volume cover image (custom) + /// + /// + /// + public static string GetVolumeFormat(int volumeId) + { + return $"v{volumeId}"; + } + + /// + /// 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) + { + // ReSharper disable once StringLiteralTypo + return $"readinglist{readingListId}"; + } + + /// + /// Returns the name format for a thumbnail (temp thumbnail) + /// + /// + /// + public static string GetThumbnailFormat(int chapterId) + { + return $"thumbnail{chapterId}"; + } + + public static string GetWebLinkFormat(string url, EncodeFormat encodeFormat) + { + return $"{new Uri(url).Host.Replace("www.", string.Empty)}{encodeFormat.GetExtension()}"; + } + + public static string GetPublisherFormat(string publisher, EncodeFormat encodeFormat) + { + return $"{publisher}{encodeFormat.GetExtension()}"; + } + + /// + public void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100) + { + var (width, height) = size.GetDimensions(); + int rows, cols; + + if (coverImages.Count == 1) + { + rows = 1; + cols = 1; + } + else if (coverImages.Count == 2) + { + rows = 1; + cols = 2; + } + else + { + rows = 2; + cols = 2; + } + + + var image = _imageFactory.Create(width, height); + + var thumbnailWidth = image.Width / cols; + var thumbnailHeight = image.Height / rows; + + for (var i = 0; i < coverImages.Count; i++) + { + if (!File.Exists(coverImages[i])) continue; + var tile = _imageFactory.Create(coverImages[i]); + tile.Thumbnail(thumbnailWidth, thumbnailHeight); + + var row = i / cols; + var col = i % cols; + + var x = col * thumbnailWidth; + var y = row * thumbnailHeight; + + if (coverImages.Count == 3 && i == 2) + { + x = (image.Width - thumbnailWidth) / 2; + y = thumbnailHeight; + } + image.Composite(tile,x,y); + } + + image.Save(dest, format, quality); + } + + /// + public void UpdateColorScape(IHasCoverImage entity) + { + var colors = CalculateColorScape(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); + entity.PrimaryColor = colors.Primary; + entity.SecondaryColor = colors.Secondary; + } + + public static Color HexToRgb(string? hex) + { + if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null"); + + // Remove the leading '#' if present + hex = hex.TrimStart('#'); + + // Ensure the hex string is valid + if (hex.Length != 6 && hex.Length != 3) + { + throw new ArgumentException("Hex string should be 6 or 3 characters long."); + } + + if (hex.Length == 3) + { + // Expand shorthand notation to full form (e.g., "abc" -> "aabbcc") + hex = string.Concat(hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]); + } + + // Parse the hex string into RGB components + var r = Convert.ToInt32(hex.Substring(0, 2), 16); + var g = Convert.ToInt32(hex.Substring(2, 2), 16); + var b = Convert.ToInt32(hex.Substring(4, 2), 16); + + return Color.FromArgb(r, g, b); + } + + +} diff --git a/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs b/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs index 81eaea5b4b..504022bbfe 100644 --- a/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs +++ b/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs @@ -27,8 +27,11 @@ public class ImageMagickImage : IImage /// An instance of . public static IImage CreateFromBase64(string base64) { + if (string.IsNullOrEmpty(base64)) + throw new ArgumentNullException(nameof(base64)); + ImageMagickImage m = new ImageMagickImage(); - m._image = (MagickImage)MagickImage.FromBase64(base64); + m._image = (MagickImage) MagickImage.FromBase64(base64); return m; } @@ -41,7 +44,7 @@ public static IImage CreateFromBase64(string base64) /// An instance of . public static IImage CreateFromBGRAByteArray(byte[] bgraByteArray, int width, int height) { - //Convert to RGBA float array (Image Magick 16 uses float array with values from 0-65535) + // Convert to RGBA float array (Image Magick 16 uses float array with values from 0-65535) var floats = new float[bgraByteArray.Length]; for (var i = 0; i < bgraByteArray.Length; i += 4) { @@ -50,10 +53,12 @@ public static IImage CreateFromBGRAByteArray(byte[] bgraByteArray, int width, in floats[i + 2] = bgraByteArray[i] << 8; floats[i + 3] = bgraByteArray[i + 3] << 8; } + ImageMagickImage m = new ImageMagickImage(); m._image = new MagickImage(MagickColor.FromRgba(0, 0, 0, 0), width, height); using var pixels = m._image.GetPixels(); pixels.SetArea(0, 0, width, height, floats); + return m; } @@ -85,7 +90,7 @@ public ImageMagickImage(Stream stream) /// The existing . public ImageMagickImage(MagickImage image) { - _image = (MagickImage)image.Clone(); + _image = (MagickImage) image.Clone(); } /// @@ -164,7 +169,7 @@ public Task SaveAsync(Stream stream, EncodeFormat format, int quality, Cancellat /// public float[] GetRGBAImageData() { - float[] data = null; + float[] data = []; float scale = 1.0f / 256; if (_image.ChannelCount == 4) { diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 71be6078ed..9d647445e3 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -26,6 +26,13 @@ public static class Parser public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); + /// + /// Mime Mappings on Browsers Non Universally Supported Image Formats. + /// Examples: + /// Browser presents jp2, which means it supports images with file extensions jp2 and j2k. (JPEG200) + /// Browser present heif, which means it supports images with file extensions heif and heic (HEIF) + /// Browser present jxl, which means it supports images with file extension jxl (JPEG-XL) + /// public static Dictionary> NonUniversalSupportedMimeMappings = new Dictionary>() { { "jp2", ["jp2", "j2k"] } , @@ -35,9 +42,22 @@ public static class Parser { "jxl", ["jxl"] } , { "avif", ["avif"] } , }; - public static string[] NonUniversalFileImageExtensionArray = { "avif", "jxl", "heif", "heic", "j2k", "jp2" }; + + /// + /// Browser Universally Supported Image extensions that we support. Means, all browsers support this image formats. + /// public static string[] UniversalFileImageExtensionArray = { "png", "jpeg", "jpg", "webp", "gif" }; + /// + /// Browser Non Universally Supported Image extensions that we support. + /// + public static string[] NonUniversalFileImageExtensionArray = { "avif", "jxl", "heif", "heic", "j2k", "jp2" }; + /// + /// Regex to Match Non Universally supported Images extensions. + /// public static string NonUniversalFileImageExtensions = @"^(\." + string.Join(@"|\.", NonUniversalFileImageExtensionArray) + ")"; + /// + /// Regex to Match All our supported Images extensions. + /// public static string ImageFileExtensions = @"^(\." + string.Join(@"|\.", UniversalFileImageExtensionArray.Union(NonUniversalFileImageExtensionArray)) + ")"; // Don't forget to update CoverChooser From fbbbbb8e8bb3d70d05e68efebdae3c7224856920 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Thu, 10 Oct 2024 18:14:16 -0300 Subject: [PATCH 28/37] Removed bak files, fixed EncodeFormat BOM --- API/Entities/Enums/EncodeFormat.cs | 2 +- API/Services/ImageService.cs.bak | 994 ----------------------------- 2 files changed, 1 insertion(+), 995 deletions(-) delete mode 100644 API/Services/ImageService.cs.bak diff --git a/API/Entities/Enums/EncodeFormat.cs b/API/Entities/Enums/EncodeFormat.cs index 0d61409276..fa4e97130a 100644 --- a/API/Entities/Enums/EncodeFormat.cs +++ b/API/Entities/Enums/EncodeFormat.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; namespace API.Entities.Enums; diff --git a/API/Services/ImageService.cs.bak b/API/Services/ImageService.cs.bak deleted file mode 100644 index 4b512a79a1..0000000000 --- a/API/Services/ImageService.cs.bak +++ /dev/null @@ -1,994 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using API.Constants; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Extensions; -using API.Services.ImageServices; -using API.Services.Tasks.Scanner.Parser; -using EasyCaching.Core; -using Flurl; -using Flurl.Http; -using HtmlAgilityPack; -using Kavita.Common; -using Microsoft.EntityFrameworkCore.Migrations.Operations; -using Microsoft.Extensions.Logging; - - -namespace API.Services; -/// -/// Interface for the ImageService. -/// -public interface IImageService -{ - /// - /// Extracts images from a file and copies them to the target directory. - /// - /// The file path of the source file. - /// The target directory to copy the images to. - /// The number of files to extract. Default is 1. - void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); - - /// - /// Gets the cover image from the specified path and saves it to the output directory. - /// - /// The path of the image file. - /// The name of the output file. - /// The output directory to save the image. - /// The encoding format to convert and save the image. - /// The size of the cover image. - /// The quality of the image. Default is 100. - /// The file name with extension of the saved image. - string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100); - - /// - /// Creates a thumbnail version of a base64 encoded image. - /// - /// The base64 encoded image. - /// The name of the output file. - /// The encoding format to convert and save the image. - /// The width of the thumbnail. Default is 320. - /// The quality of the image. Default is 100. - /// The file name with extension of the saved thumbnail image. - string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, int quality = 100); - - /// - /// Creates a thumbnail out of a memory stream and saves to with the passed - /// fileName and the appropriate 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 the passed encoding - /// The size of the cover image. - /// The quality of the image. Default is 100. - /// File name with extension of the file. This will always write to - string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); - - /// - /// Writes out a thumbnail image from a file path input and saves to with the passed - /// - /// The path of the source image file. - /// filename to save as without extension - /// Where to output the file, defaults to covers directory - /// Export the file as the passed encoding - /// The size of the cover image. - /// The quality of the image. Default is 100. - /// File name with extension of the file. This will always write to - string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); - - /// - /// Converts the specified image file to the specified encoding format and saves it to the output path. - /// - /// The full path of the image file to convert. - /// The path to save the converted image file. - /// The encoding format to convert the image. - /// The quality of the image. Default is 100. - /// The file path of the converted image file. - Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100); - - /// - /// Checks if the specified file is an image. - /// - /// The path of the file to check. - /// True if the file is an image; otherwise, false. - Task IsImage(string filePath); - - /// - /// Downloads the favicon from the specified URL and saves it to the output directory. - /// - /// The URL of the favicon. - /// The encoding format to convert and save the favicon. - /// The quality of the favicon. Default is 100. - /// The file name with extension of the saved favicon. - Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100); - - /// - /// Downloads the publisher image for the specified publisher name and saves it to the output directory. - /// - /// The name of the publisher. - /// The encoding format to convert and save the publisher image. - /// The quality of the publisher image. Default is 100. - /// The file name with extension of the saved publisher image. - Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100); - - /// - /// Updates the color scape of an entity with a cover image. - /// - /// The entity with a cover image. - void UpdateColorScape(IHasCoverImage entity); - - /// - /// Replaces the file format of an image with the specified supported image formats by the browser, if needed, otherwise original file will be served. - /// - /// The name of the image file. - /// The list of supported image formats by the browser. - /// The encoding format to convert the image. Default is JPG - /// The quality of the image. Default is 99. - /// The file name with extension of the replaced image file. - string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99); - - /// - /// Creates a merged image from a list of cover images and saves it to the specified destination. - /// - /// The list of cover images. - /// The size of the merged image. - /// The destination path to save the merged image. - /// The encoding format to convert and save the merged image. Default is PNG. - /// The quality of the merged image. Default is 100. - void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100); - - /// - /// The image factory used to create and manipulate images. - /// - IImageFactory ImageFactory { get; } -} - -public class ImageService : IImageService -{ - public const string Name = "ImageService"; - 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+"; - private const double WhiteThreshold = 0.95; // Colors with lightness above this are considered too close to white - private const double BlackThreshold = 0.25; // Colors with lightness below this are considered too close to black - - /// - /// Width of the Thumbnail generation - /// - private const int ThumbnailWidth = 320; - /// - /// Height of the Thumbnail generation - /// - private const int ThumbnailHeight = 455; - /// - /// Width of a cover for Library - /// - public const int LibraryThumbnailWidth = 32; - - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IEasyCachingProviderFactory _cacheFactory; - private readonly IImageFactory _imageFactory; - - private static readonly string[] ValidIconRelations = { - "icon", - "apple-touch-icon", - "apple-touch-icon-precomposed", - "apple-touch-icon icon-precomposed" // ComicVine has it combined - }; - - /// - /// A mapping of urls that need to get the icon from another url, due to strangeness (like app.plex.tv loading a black icon) - /// - private static readonly IDictionary FaviconUrlMapper = new Dictionary - { - ["https://app.plex.tv"] = "https://plex.tv" - }; - - - - private static NamedMonitor _lock = new NamedMonitor(); - /// - /// Represents a named monitor that provides thread-safe access to objects based on their names. - /// - class NamedMonitor - { - readonly ConcurrentDictionary _dictionary = new ConcurrentDictionary(); - - public object this[string name] => _dictionary.GetOrAdd(name, _ => new object()); - } - - - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The directory service. - /// The cache factory. - /// The image factory. - public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory, IImageFactory imageFactory) - { - _logger = logger; - _directoryService = directoryService; - _cacheFactory = cacheFactory; - _imageFactory = imageFactory; - } - - public IImageFactory ImageFactory => _imageFactory; - - /// - 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); - } - } - - /// - /// Creates a thumbnail image from the specified image with the given width and height. - /// If the image aspect ratio is significantly different from the target aspect ratio, it will be context aware cropped to fit. - /// - /// The image to create a thumbnail from. - /// The width of the thumbnail. - /// The height of the thumbnail. - /// The thumbnail image. - public static IImage Thumbnail(IImage image, int width, int height) - { - try - { - if (WillScaleWell(image, width, height) || IsLikelyWideImage(image.Width, image.Height)) - { - image.Thumbnail(width, height); - return image; - } - } - catch (Exception) - { - /* Swallow */ - } - var crop = SmartCrop.Crop(image, new SmartCrop.SmartCropOptions { Width = width, Height = height }); - if (crop.TopCrop.Width != width && crop.TopCrop.Height != height) - { - image.Crop(crop.TopCrop.X, crop.TopCrop.Y, crop.TopCrop.Width, crop.TopCrop.Height); - } - image.Thumbnail(width, height); - return image; - } - - public static bool WillScaleWell(IImage sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) - { - // Calculate the aspect ratios - var sourceAspectRatio = (double) sourceImage.Width / sourceImage.Height; - var targetAspectRatio = (double) targetWidth / targetHeight; - - // Compare aspect ratios - if (Math.Abs(sourceAspectRatio - targetAspectRatio) > tolerance) - { - return false; // Aspect ratios differ significantly - } - - // Calculate scaling factors - var widthScaleFactor = (double) targetWidth / sourceImage.Width; - var heightScaleFactor = (double) targetHeight / sourceImage.Height; - - // Check resolution quality (example thresholds) - if (widthScaleFactor > 2.0 || heightScaleFactor > 2.0) - { - return false; // Scaling factor too large - } - - return true; // Image will scale well - } - - private static bool IsLikelyWideImage(int width, int height) - { - var aspectRatio = (double) width / height; - return aspectRatio > 1.25; - } - - - /// - public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100) - { - if (string.IsNullOrEmpty(path)) return string.Empty; - - try - { - var (width, height) = size.GetDimensions(); - using var thumbnail = Thumbnail(_imageFactory.Create(path), width, height); - var filename = fileName + encodeFormat.GetExtension(); - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); - 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; - } - - /// - public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) - { - var (targetWidth, targetHeight) = size.GetDimensions(); - if (stream.CanSeek) stream.Position = 0; - - using var thumbnail = Thumbnail(_imageFactory.Create(stream), targetWidth, targetHeight); - var filename = fileName + encodeFormat.GetExtension(); - _directoryService.ExistOrCreate(outputDirectory); - - try - { - _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - } catch (Exception) {/* Swallow exception */} - - try - { - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); - } - catch (Exception) {/* Swallow exception */} - - return filename; - } - /// - public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) - { - var (width, height) = size.GetDimensions(); - using var thumbnail = Thumbnail(_imageFactory.Create(sourceFile), width, height); - var filename = fileName + encodeFormat.GetExtension(); - _directoryService.ExistOrCreate(outputDirectory); - try - { - _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - } catch (Exception) {/* Swallow exception */} - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); - - return filename; - } - /// - public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100) - { - var file = _directoryService.FileSystem.FileInfo.New(filePath); - var fileName = file.Name.Replace(file.Extension, string.Empty); - var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); - using var sourceImage = _imageFactory.Create(filePath); - await sourceImage.SaveAsync(outputFile, encodeFormat, quality).ConfigureAwait(false); - - return outputFile; - } - - /// - public Task IsImage(string filePath) - { - try - { - return Task.FromResult(_imageFactory.GetDimensions(filePath) != null); - } - catch (Exception) - { - /* Swallow Exception */ - } - - return Task.FromResult(false); - } - /// - public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100) - { - // Parse the URL to get the domain (including subdomain) - var uri = new Uri(url); - var domain = uri.Host.Replace(Environment.NewLine, string.Empty); - var baseUrl = uri.Scheme + "://" + uri.Host; - - - var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon); - var res = await provider.GetAsync(baseUrl); - if (res.HasValue) - { - _logger.LogInformation("Kavita has already tried to fetch from {BaseUrl} and failed. Skipping duplicate check", baseUrl); - throw new KavitaException($"Kavita has already tried to fetch from {baseUrl} and failed. Skipping duplicate check"); - } - - await provider.SetAsync(baseUrl, string.Empty, TimeSpan.FromDays(10)); - if (FaviconUrlMapper.TryGetValue(baseUrl, out var value)) - { - url = value; - } - - var correctSizeLink = string.Empty; - - try - { - var htmlContent = url.GetStringAsync().Result; - var htmlDocument = new HtmlDocument(); - htmlDocument.LoadHtml(htmlContent); - var pngLinks = htmlDocument.DocumentNode.Descendants("link") - .Where(link => ValidIconRelations.Contains(link.GetAttributeValue("rel", string.Empty))) - .Select(link => link.GetAttributeValue("href", string.Empty)) - .Where(href => href.Split("?")[0].EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) - .ToList(); - - correctSizeLink = (pngLinks?.Find(pngLink => pngLink.Contains("32")) ?? pngLinks?.FirstOrDefault()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error downloading favicon.png for {Domain}, will try fallback methods", domain); - } - - try - { - if (string.IsNullOrEmpty(correctSizeLink)) - { - correctSizeLink = await FallbackToKavitaReaderFavicon(baseUrl); - } - if (string.IsNullOrEmpty(correctSizeLink)) - { - throw new KavitaException($"Could not grab favicon from {baseUrl}"); - } - - var finalUrl = correctSizeLink; - - // If starts with //, it's coming usually from an offsite cdn - if (correctSizeLink.StartsWith("//")) - { - finalUrl = "https:" + correctSizeLink; - } - else if (!correctSizeLink.StartsWith(uri.Scheme)) - { - finalUrl = Url.Combine(baseUrl, correctSizeLink); - } - - _logger.LogTrace("Fetching favicon from {Url}", finalUrl); - // Download the favicon.ico file using Flurl - var faviconStream = await finalUrl - .AllowHttpStatus("2xx,304") - .GetStreamAsync(); - - // Create the destination file path - using var image = _imageFactory.Create(faviconStream); - var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); - await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); - _logger.LogDebug("Favicon for {Domain} downloaded and saved successfully", domain); - return filename; - } catch (Exception ex) - { - _logger.LogError(ex, "Error downloading favicon for {Domain}", domain); - throw; - } - } - /// - public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100) - { - try - { - var publisherLink = await FallbackToKavitaReaderPublisher(publisherName); - if (string.IsNullOrEmpty(publisherLink)) - { - throw new KavitaException($"Could not grab publisher image for {publisherName}"); - } - - var finalUrl = publisherLink; - - _logger.LogTrace("Fetching publisher image from {Url}", finalUrl); - // Download the favicon.ico file using Flurl - var publisherStream = await finalUrl - .AllowHttpStatus("2xx,304") - .GetStreamAsync(); - - // Create the destination file path - using var image = _imageFactory.Create(publisherStream); - var filename = GetPublisherFormat(publisherName, encodeFormat); - await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); - _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName); - return filename; - } catch (Exception ex) - { - _logger.LogError(ex, "Error downloading image for {PublisherName}", publisherName); - throw; - } - } - - private (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) - { - - - var rgbPixels = _imageFactory.GetRgbPixelsPercentage(imagePath, 10); - - // Perform k-means clustering - var clusters = KMeansClustering(rgbPixels, 4); - - var sorted = SortByVibrancy(clusters); - - // Ensure white and black are not selected as primary/secondary colors - sorted = sorted.Where(c => !IsCloseToWhiteOrBlack(c)).ToList(); - - if (sorted.Count >= 2) - { - return (sorted[0], sorted[1]); - } - if (sorted.Count == 1) - { - return (sorted[0], null); - } - - return (null, null); - } - - private static bool IsColorCloseToWhiteOrBlack(Vector3 color) - { - var (_, _, lightness) = RgbToHsl(color); - return lightness is > WhiteThreshold or < BlackThreshold; - } - - private static List KMeansClustering(List points, int k, int maxIterations = 100) - { - var random = new Random(); - var centroids = points.OrderBy(x => random.Next()).Take(k).ToList(); - - for (var i = 0; i < maxIterations; i++) - { - var clusters = new List[k]; - for (var j = 0; j < k; j++) - { - clusters[j] = []; - } - - foreach (var point in points) - { - var nearestCentroidIndex = centroids - .Select((centroid, index) => new { Index = index, Distance = Vector3.DistanceSquared(centroid, point) }) - .OrderBy(x => x.Distance) - .First().Index; - clusters[nearestCentroidIndex].Add(point); - } - - var newCentroids = clusters.Select(cluster => - cluster.Count != 0 ? new Vector3( - cluster.Average(p => p.X), - cluster.Average(p => p.Y), - cluster.Average(p => p.Z) - ) : Vector3.Zero - ).ToList(); - - if (centroids.SequenceEqual(newCentroids)) - break; - - centroids = newCentroids; - } - - return centroids; - } - - public static List SortByBrightness(List colors) - { - return colors.OrderBy(c => 0.299 * c.X + 0.587 * c.Y + 0.114 * c.Z).ToList(); - } - - private static List SortByVibrancy(List colors) - { - return colors.OrderByDescending(c => - { - var max = Math.Max(c.X, Math.Max(c.Y, c.Z)); - var min = Math.Min(c.X, Math.Min(c.Y, c.Z)); - return (max - min) / max; - }).ToList(); - } - - private static bool IsCloseToWhiteOrBlack(Vector3 color) - { - var threshold = 30; - return (color.X > 255 - threshold && color.Y > 255 - threshold && color.Z > 255 - threshold) || - (color.X < threshold && color.Y < threshold && color.Z < threshold); - } - - private static string RgbToHex(Vector3 color) - { - return $"#{(int)color.X:X2}{(int)color.Y:X2}{(int)color.Z:X2}"; - } - - private static Vector3 GetComplementaryColor(Vector3 color) - { - // Convert RGB to HSL - var (h, s, l) = RgbToHsl(color); - - // Rotate hue by 180 degrees - h = (h + 180) % 360; - - // Convert back to RGB - return HslToRgb(h, s, l); - } - - private static (double H, double S, double L) RgbToHsl(Vector3 rgb) - { - double r = rgb.X / 255; - double g = rgb.Y / 255; - double b = rgb.Z / 255; - - var max = Math.Max(r, Math.Max(g, b)); - var min = Math.Min(r, Math.Min(g, b)); - var diff = max - min; - - double h = 0; - double s = 0; - var l = (max + min) / 2; - - if (Math.Abs(diff) > 0.00001) - { - s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min); - - if (max == r) - h = (g - b) / diff + (g < b ? 6 : 0); - else if (max == g) - h = (b - r) / diff + 2; - else if (max == b) - h = (r - g) / diff + 4; - - h *= 60; - } - - return (h, s, l); - } - - private static Vector3 HslToRgb(double h, double s, double l) - { - double r, g, b; - - if (Math.Abs(s) < 0.00001) - { - r = g = b = l; - } - else - { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = HueToRgb(p, q, h + 120); - g = HueToRgb(p, q, h); - b = HueToRgb(p, q, h - 120); - } - - return new Vector3((float)(r * 255), (float)(g * 255), (float)(b * 255)); - } - - private static double HueToRgb(double p, double q, double t) - { - if (t < 0) t += 360; - if (t > 360) t -= 360; - return t switch - { - < 60 => p + (q - p) * t / 60, - < 180 => q, - < 240 => p + (q - p) * (240 - t) / 60, - _ => p - }; - } - - /// - /// Generates the Primary and Secondary colors from a file - /// - /// This may use a second most common color or a complementary color. It's up to implemenation to choose what's best - /// - /// - public ColorScape CalculateColorScape(string sourceFile) - { - if (!File.Exists(sourceFile)) return new ColorScape() {Primary = null, Secondary = null}; - - var colors = GetPrimarySecondaryColors(sourceFile); - - return new ColorScape() - { - Primary = colors.Item1 == null ? null : RgbToHex(colors.Item1.Value), - Secondary = colors.Item2 == null ? null : RgbToHex(colors.Item2.Value) - }; - } - private bool CheckDirectSupport(string filename, List supportedImageFormats) - { - if (supportedImageFormats == null) return false; - - string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1); - return supportedImageFormats.Contains(ext); - } - - - /// - public string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99) - { - if (CheckDirectSupport(filename, supportedImageFormats)) return filename; - - Match m = Regex.Match(Path.GetExtension(filename), Parser.NonUniversalFileImageExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); - if (!m.Success) return filename; - - string destination = Path.ChangeExtension(filename, format.GetExtension().Substring(1)); - - // Adding a lock per destination, sometimes web ui triggers same image loading at the ~ same time. - // So, if other thread is already processing the image, we should wait for it to finish, then the File.Exists(destination) will early exit. - // This exists, to prevent multiple threads from processing the same image at the same time. - lock (_lock[destination]) - { - if (File.Exists(destination)) - { - // Destination already exist, the conversion was already made. - return destination; - } - using var sourceImage = _imageFactory.Create(filename); - sourceImage.Save(destination, format, quality); - try - { - File.Delete(filename); - } - catch (Exception) { /* Swallow Exception */ } - } - return destination; - } - - private static async Task FallbackToKavitaReaderFavicon(string baseUrl) - { - var correctSizeLink = string.Empty; - var allOverrides = await "https://www.kavitareader.com/assets/favicons/urls.txt".GetStringAsync(); - if (!string.IsNullOrEmpty(allOverrides)) - { - var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty); - var externalFile = allOverrides - .Split("\n") - .FirstOrDefault(url => - cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) || - cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty) - )); - - if (string.IsNullOrEmpty(externalFile)) - { - throw new KavitaException($"Could not grab favicon from {baseUrl}"); - } - - correctSizeLink = "https://www.kavitareader.com/assets/favicons/" + externalFile; - } - - return correctSizeLink; - } - - private static async Task FallbackToKavitaReaderPublisher(string publisherName) - { - var externalLink = string.Empty; - var allOverrides = await "https://www.kavitareader.com/assets/publishers/publishers.txt".GetStringAsync(); - if (!string.IsNullOrEmpty(allOverrides)) - { - var externalFile = allOverrides - .Split("\n") - .Select(publisherLine => - { - var tokens = publisherLine.Split("|"); - if (tokens.Length != 2) return null; - var aliases = tokens[0]; - // Multiple publisher aliases are separated by # - if (aliases.Split("#").Any(name => name.ToLowerInvariant().Trim().Equals(publisherName.ToLowerInvariant().Trim()))) - { - return tokens[1]; - } - return null; - }) - .FirstOrDefault(url => !string.IsNullOrEmpty(url)); - - if (string.IsNullOrEmpty(externalFile)) - { - throw new KavitaException($"Could not grab publisher image for {publisherName}"); - } - - externalLink = "https://www.kavitareader.com/assets/publishers/" + externalFile; - } - - return externalLink; - } - - /// - public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth, int quality = 100) - { - try - { - - using var thumbnail = _imageFactory.CreateFromBase64(encodedImage); - int thumbnailHeight = (int)(thumbnail.Height * ((double)thumbnailWidth / thumbnail.Width)); - thumbnail.Thumbnail(thumbnailWidth, thumbnailHeight); - fileName += encodeFormat.GetExtension(); - thumbnail.Save(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName), encodeFormat, quality); - 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 volume cover image (custom) - /// - /// - /// - public static string GetVolumeFormat(int volumeId) - { - return $"v{volumeId}"; - } - - /// - /// 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) - { - // ReSharper disable once StringLiteralTypo - return $"readinglist{readingListId}"; - } - - /// - /// Returns the name format for a thumbnail (temp thumbnail) - /// - /// - /// - public static string GetThumbnailFormat(int chapterId) - { - return $"thumbnail{chapterId}"; - } - - public static string GetWebLinkFormat(string url, EncodeFormat encodeFormat) - { - return $"{new Uri(url).Host.Replace("www.", string.Empty)}{encodeFormat.GetExtension()}"; - } - - public static string GetPublisherFormat(string publisher, EncodeFormat encodeFormat) - { - return $"{publisher}{encodeFormat.GetExtension()}"; - } - - /// - public void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100) - { - var (width, height) = size.GetDimensions(); - int rows, cols; - - if (coverImages.Count == 1) - { - rows = 1; - cols = 1; - } - else if (coverImages.Count == 2) - { - rows = 1; - cols = 2; - } - else - { - rows = 2; - cols = 2; - } - - - var image = _imageFactory.Create(width, height); - - var thumbnailWidth = image.Width / cols; - var thumbnailHeight = image.Height / rows; - - for (var i = 0; i < coverImages.Count; i++) - { - if (!File.Exists(coverImages[i])) continue; - var tile = _imageFactory.Create(coverImages[i]); - tile.Thumbnail(thumbnailWidth, thumbnailHeight); - - var row = i / cols; - var col = i % cols; - - var x = col * thumbnailWidth; - var y = row * thumbnailHeight; - - if (coverImages.Count == 3 && i == 2) - { - x = (image.Width - thumbnailWidth) / 2; - y = thumbnailHeight; - } - image.Composite(tile,x,y); - } - - image.Save(dest, format, quality); - } - - /// - public void UpdateColorScape(IHasCoverImage entity) - { - var colors = CalculateColorScape(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); - entity.PrimaryColor = colors.Primary; - entity.SecondaryColor = colors.Secondary; - } - - public static Color HexToRgb(string? hex) - { - if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null"); - - // Remove the leading '#' if present - hex = hex.TrimStart('#'); - - // Ensure the hex string is valid - if (hex.Length != 6 && hex.Length != 3) - { - throw new ArgumentException("Hex string should be 6 or 3 characters long."); - } - - if (hex.Length == 3) - { - // Expand shorthand notation to full form (e.g., "abc" -> "aabbcc") - hex = string.Concat(hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]); - } - - // Parse the hex string into RGB components - var r = Convert.ToInt32(hex.Substring(0, 2), 16); - var g = Convert.ToInt32(hex.Substring(2, 2), 16); - var b = Convert.ToInt32(hex.Substring(4, 2), 16); - - return Color.FromArgb(r, g, b); - } - - -} From a0f804ba175cda90d40a38219ef4a81589c12756 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Fri, 11 Oct 2024 20:30:40 -0300 Subject: [PATCH 29/37] Removed quality parameter everywhere. Added Quality From EncodedFormat Extension --- API.Benchmark/ArchiveServiceBenchmark.cs | 6 +- API.Tests/Services/ArchiveServiceTests.cs | 2 +- API.Tests/Services/ImageServiceTests.cs | 4 +- API/Extensions/EncodeFormatExtensions.cs | 12 ++++ API/Services/BookService.cs | 2 +- API/Services/ImageService.cs | 63 ++++++++----------- API/Services/ImageServices/IImage.cs | 8 +-- .../ImageMagick/ImageMagickImage.cs | 17 ++--- 8 files changed, 59 insertions(+), 55 deletions(-) diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index 589433360a..dd7b5f1e78 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -26,7 +26,7 @@ public class ArchiveServiceBenchmark private readonly IImageService _imageService; private readonly IImageFactory _imageFactory; private const string SourceImage = "Data/comic-normal.jpg"; - + public ArchiveServiceBenchmark() { @@ -71,7 +71,7 @@ public void ImageMagick_ExtractImage_PNG() int width = 320; int height = (int)(thumbnail2.Height * (width / (double)thumbnail2.Width)); thumbnail2.Thumbnail(width, height); - thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png"), EncodeFormat.PNG, 100); + thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png"), EncodeFormat.PNG); } [Benchmark] @@ -85,7 +85,7 @@ public void ImageMagick_ExtractImage_WebP() int width = 320; int height = (int)(thumbnail2.Height * (width / (double)thumbnail2.Width)); thumbnail2.Thumbnail(width, height); - thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp"), EncodeFormat.PNG, 100); + thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp"), EncodeFormat.PNG); } diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 497effad15..9fd1772cb9 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -177,7 +177,7 @@ public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFi int height = (int)(thumbnail.Height * (width / (double)thumbnail.Width)); thumbnail.Thumbnail(width, height); using MemoryStream stream = new MemoryStream(); - thumbnail.Save(stream, EncodeFormat.PNG, 100); + thumbnail.Save(stream, EncodeFormat.PNG); var expectedBytes = stream.ToArray(); archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default); diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index 2497f02be9..77f79ca574 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -63,7 +63,7 @@ private void GenerateFiles(string outputExtension) var thumbnail = factory.Create(imagePath); thumbnail = ImageService.Thumbnail(thumbnail, dims.Width, dims.Height); var outputFileName = fileName + outputExtension + ".png"; - thumbnail.Save(Path.Join(_testDirectory, outputFileName), EncodeFormat.PNG,100); + thumbnail.Save(Path.Join(_testDirectory, outputFileName), EncodeFormat.PNG); } } @@ -166,7 +166,7 @@ private static void GenerateColorImage(string hexColor, string outputPath) ImageMagickImageFactory factory = new ImageMagickImageFactory(); var color = ImageService.HexToRgb(hexColor); using var colorImage = factory.Create(200,100,color.R, color.G, color.B); - colorImage.Save(outputPath, EncodeFormat.PNG, 100); + colorImage.Save(outputPath, EncodeFormat.PNG); } private void GenerateHtmlFileForColorScape() diff --git a/API/Extensions/EncodeFormatExtensions.cs b/API/Extensions/EncodeFormatExtensions.cs index 21b57ef43e..b4372d5f99 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/API/Extensions/EncodeFormatExtensions.cs @@ -17,4 +17,16 @@ public static string GetExtension(this EncodeFormat encodeFormat) _ => throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null) }; } + + public static int DefaultQuality(this EncodeFormat encodeFormat) + { + return encodeFormat switch + { + EncodeFormat.PNG => 100, // (Image Magick Maximum Deflate Compression) (In case of PNG, png is always lossless, Quality indicate the compression level) + EncodeFormat.WEBP => 100, + EncodeFormat.AVIF => 100, + EncodeFormat.JPEG => 99, // (Best Compression speed, with almost no visual quality loss) + _ => throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null) + }; + } } diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 4df3f422ff..31703b2a17 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -1280,7 +1280,7 @@ private void GetPdfPage(IDocReader docReader, int pageNumber, Stream stream) var height = pageReader.GetPageHeight(); IImage image = _imageService.ImageFactory.CreateFromBGRAByteArray(rawBytes, width, height); stream.Seek(0, SeekOrigin.Begin); - image.Save(stream, EncodeFormat.PNG, 100); + image.Save(stream, EncodeFormat.PNG); stream.Seek(0, SeekOrigin.Begin); } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 0f8991b5ae..73fbac60f0 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -49,9 +49,8 @@ public interface IImageService /// The output directory to save the image. /// The encoding format to convert and save the image. /// The size of the cover image. - /// The quality of the image. Default is 100. /// The file name with extension of the saved image. - string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100); + string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size); /// /// Creates a thumbnail version of a base64 encoded image. @@ -60,9 +59,8 @@ public interface IImageService /// The name of the output file. /// The encoding format to convert and save the image. /// The width of the thumbnail. Default is 320. - /// The quality of the image. Default is 100. /// The file name with extension of the saved thumbnail image. - string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, int quality = 100); + string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320); /// /// Creates a thumbnail out of a memory stream and saves to with the passed @@ -73,9 +71,8 @@ public interface IImageService /// Where to output the file, defaults to covers directory /// Export the file as the passed encoding /// The size of the cover image. - /// The quality of the image. Default is 100. /// File name with extension of the file. This will always write to - string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); + string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); /// /// Writes out a thumbnail image from a file path input and saves to with the passed @@ -85,9 +82,8 @@ public interface IImageService /// Where to output the file, defaults to covers directory /// Export the file as the passed encoding /// The size of the cover image. - /// The quality of the image. Default is 100. /// File name with extension of the file. This will always write to - string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100); + string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); /// /// Converts the specified image file to the specified encoding format and saves it to the output path. @@ -95,9 +91,8 @@ public interface IImageService /// The full path of the image file to convert. /// The path to save the converted image file. /// The encoding format to convert the image. - /// The quality of the image. Default is 100. /// The file path of the converted image file. - Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100); + Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat); /// /// Checks if the specified file is an image. @@ -111,18 +106,16 @@ public interface IImageService /// /// The URL of the favicon. /// The encoding format to convert and save the favicon. - /// The quality of the favicon. Default is 100. /// The file name with extension of the saved favicon. - Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100); + Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat); /// /// Downloads the publisher image for the specified publisher name and saves it to the output directory. /// /// The name of the publisher. /// The encoding format to convert and save the publisher image. - /// The quality of the publisher image. Default is 100. /// The file name with extension of the saved publisher image. - Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100); + Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat); /// /// Updates the color scape of an entity with a cover image. @@ -136,9 +129,8 @@ public interface IImageService /// The name of the image file. /// The list of supported image formats by the browser. /// The encoding format to convert the image. Default is JPG - /// The quality of the image. Default is 99. /// The file name with extension of the replaced image file. - string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99); + string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG); /// /// Creates a merged image from a list of cover images and saves it to the specified destination. @@ -147,8 +139,7 @@ public interface IImageService /// The size of the merged image. /// The destination path to save the merged image. /// The encoding format to convert and save the merged image. Default is PNG. - /// The quality of the merged image. Default is 100. - void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100); + void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG); /// /// The image factory used to create and manipulate images. @@ -311,7 +302,7 @@ private static bool IsLikelyWideImage(int width, int height) /// - public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size, int quality = 100) + public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size) { if (string.IsNullOrEmpty(path)) return string.Empty; @@ -320,7 +311,7 @@ public string GetCoverImage(string path, string fileName, string outputDirectory var (width, height) = size.GetDimensions(); using var thumbnail = Thumbnail(_imageFactory.Create(path), width, height); var filename = fileName + encodeFormat.GetExtension(); - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat); return filename; } catch (Exception ex) @@ -332,7 +323,7 @@ public string GetCoverImage(string path, string fileName, string outputDirectory } /// - public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) + public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { var (targetWidth, targetHeight) = size.GetDimensions(); if (stream.CanSeek) stream.Position = 0; @@ -348,14 +339,14 @@ public string WriteCoverThumbnail(Stream stream, string fileName, string outputD try { - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat); } catch (Exception) {/* Swallow exception */} return filename; } /// - public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100) + public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { var (width, height) = size.GetDimensions(); using var thumbnail = Thumbnail(_imageFactory.Create(sourceFile), width, height); @@ -365,18 +356,18 @@ public string WriteCoverThumbnail(string sourceFile, string fileName, string out { _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} - thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat, quality); + thumbnail.Save(_directoryService.FileSystem.Path.Join(outputDirectory, filename), encodeFormat); return filename; } /// - public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, int quality = 100) + public async Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat) { var file = _directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); using var sourceImage = _imageFactory.Create(filePath); - await sourceImage.SaveAsync(outputFile, encodeFormat, quality).ConfigureAwait(false); + await sourceImage.SaveAsync(outputFile, encodeFormat).ConfigureAwait(false); return outputFile; } @@ -396,7 +387,7 @@ public Task IsImage(string filePath) return Task.FromResult(false); } /// - public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, int quality = 100) + public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) { // Parse the URL to get the domain (including subdomain) var uri = new Uri(url); @@ -470,7 +461,7 @@ public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFo // Create the destination file path using var image = _imageFactory.Create(faviconStream); var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); - await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); + await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat); _logger.LogDebug("Favicon for {Domain} downloaded and saved successfully", domain); return filename; } catch (Exception ex) @@ -480,7 +471,7 @@ public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFo } } /// - public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, int quality = 100) + public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat) { try { @@ -501,7 +492,7 @@ public async Task DownloadPublisherImageAsync(string publisherName, Enco // Create the destination file path using var image = _imageFactory.Create(publisherStream); var filename = GetPublisherFormat(publisherName, encodeFormat); - await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat, quality); + await image.SaveAsync(Path.Combine(_directoryService.FaviconDirectory, filename), encodeFormat); _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName); return filename; } catch (Exception ex) @@ -713,7 +704,7 @@ private bool CheckDirectSupport(string filename, List supportedImageForm /// - public string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG, int quality = 99) + public string ReplaceImageFileFormat(string filename, List supportedImageFormats = null, EncodeFormat format = EncodeFormat.JPEG) { if (CheckDirectSupport(filename, supportedImageFormats)) return filename; @@ -733,7 +724,7 @@ public string ReplaceImageFileFormat(string filename, List supportedImag return destination; } using var sourceImage = _imageFactory.Create(filename); - sourceImage.Save(destination, format, quality); + sourceImage.Save(destination, format); try { File.Delete(filename); @@ -802,7 +793,7 @@ private static async Task FallbackToKavitaReaderPublisher(string publish } /// - public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth, int quality = 100) + public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth) { try { @@ -811,7 +802,7 @@ public string CreateThumbnailFromBase64(string encodedImage, string fileName, En int thumbnailHeight = (int)(thumbnail.Height * ((double)thumbnailWidth / thumbnail.Width)); thumbnail.Thumbnail(thumbnailWidth, thumbnailHeight); fileName += encodeFormat.GetExtension(); - thumbnail.Save(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName), encodeFormat, quality); + thumbnail.Save(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName), encodeFormat); return fileName; } catch (Exception e) @@ -905,7 +896,7 @@ public static string GetPublisherFormat(string publisher, EncodeFormat encodeFor } /// - public void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG, int quality = 100) + public void CreateMergedImage(IList coverImages, CoverImageSize size, string dest, EncodeFormat format = EncodeFormat.PNG) { var (width, height) = size.GetDimensions(); int rows, cols; @@ -952,7 +943,7 @@ public void CreateMergedImage(IList coverImages, CoverImageSize size, st image.Composite(tile,x,y); } - image.Save(dest, format, quality); + image.Save(dest, format); } /// diff --git a/API/Services/ImageServices/IImage.cs b/API/Services/ImageServices/IImage.cs index 448dd26452..7fcaa4a009 100644 --- a/API/Services/ImageServices/IImage.cs +++ b/API/Services/ImageServices/IImage.cs @@ -64,7 +64,7 @@ public interface IImage : IDisposable /// The name of the file to save the image to. /// The format to save the image in. /// The quality of the saved image. - void Save(string filename, EncodeFormat format, int quality); + void Save(string filename, EncodeFormat format); /// /// Saves the image to the specified stream with the specified format and quality. @@ -72,7 +72,7 @@ public interface IImage : IDisposable /// The stream to save the image to. /// The format to save the image in. /// The quality of the saved image. - void Save(Stream stream, EncodeFormat format, int quality); + void Save(Stream stream, EncodeFormat format); /// /// Asynchronously saves the image to the specified file with the specified format and quality. @@ -82,7 +82,7 @@ public interface IImage : IDisposable /// The quality of the saved image. /// The cancellation token. /// A task representing the asynchronous save operation. - Task SaveAsync(string filename, EncodeFormat format, int quality, CancellationToken token = default); + Task SaveAsync(string filename, EncodeFormat format, CancellationToken token = default); /// /// Asynchronously saves the image to the specified stream with the specified format and quality. @@ -92,7 +92,7 @@ public interface IImage : IDisposable /// The quality of the saved image. /// The cancellation token. /// A task representing the asynchronous save operation. - Task SaveAsync(Stream stream, EncodeFormat format, int quality, CancellationToken token = default); + Task SaveAsync(Stream stream, EncodeFormat format, CancellationToken token = default); /// /// Gets the RGBA image data as an array of floats. diff --git a/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs b/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs index 504022bbfe..402762b573 100644 --- a/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs +++ b/API/Services/ImageServices/ImageMagick/ImageMagickImage.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using API.Entities.Enums; +using API.Extensions; using ImageMagick; namespace API.Services.ImageServices.ImageMagick; @@ -139,30 +140,30 @@ public void Composite(IImage overlay, int x, int y) } /// - public void Save(string filename, EncodeFormat format, int quality) + public void Save(string filename, EncodeFormat format) { - _image.Quality = quality; + _image.Quality = format.DefaultQuality(); _image.Write(filename, MagickFormatFromEncodeFormat(format)); } /// - public void Save(Stream stream, EncodeFormat format, int quality) + public void Save(Stream stream, EncodeFormat format) { - _image.Quality = quality; + _image.Quality = format.DefaultQuality(); _image.Write(stream, MagickFormatFromEncodeFormat(format)); } /// - public Task SaveAsync(string filename, EncodeFormat format, int quality, CancellationToken token = default) + public Task SaveAsync(string filename, EncodeFormat format, CancellationToken token = default) { - _image.Quality = quality; + _image.Quality = format.DefaultQuality(); return _image.WriteAsync(filename, MagickFormatFromEncodeFormat(format), token); } /// - public Task SaveAsync(Stream stream, EncodeFormat format, int quality, CancellationToken token = default) + public Task SaveAsync(Stream stream, EncodeFormat format, CancellationToken token = default) { - _image.Quality = quality; + _image.Quality = format.DefaultQuality(); return _image.WriteAsync(stream, MagickFormatFromEncodeFormat(format), token); } From 11749bd5c48a807a99667f3bc4a40c8fa4cd1f87 Mon Sep 17 00:00:00 2001 From: Maximo Piva Date: Tue, 15 Oct 2024 11:26:27 -0300 Subject: [PATCH 30/37] Remove quality parameters after refactor. --- API/Extensions/EncodeFormatExtensions.cs | 2 +- API/Services/ImageServices/IImage.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/API/Extensions/EncodeFormatExtensions.cs b/API/Extensions/EncodeFormatExtensions.cs index b4372d5f99..8dbf4dac55 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/API/Extensions/EncodeFormatExtensions.cs @@ -22,7 +22,7 @@ public static int DefaultQuality(this EncodeFormat encodeFormat) { return encodeFormat switch { - EncodeFormat.PNG => 100, // (Image Magick Maximum Deflate Compression) (In case of PNG, png is always lossless, Quality indicate the compression level) + EncodeFormat.PNG => 100, // (Image Magick Maximum Deflate Compression) (In case of PNG, png is always lossless, Quality indicates the compression level) EncodeFormat.WEBP => 100, EncodeFormat.AVIF => 100, EncodeFormat.JPEG => 99, // (Best Compression speed, with almost no visual quality loss) diff --git a/API/Services/ImageServices/IImage.cs b/API/Services/ImageServices/IImage.cs index 7fcaa4a009..afb118af99 100644 --- a/API/Services/ImageServices/IImage.cs +++ b/API/Services/ImageServices/IImage.cs @@ -63,7 +63,6 @@ public interface IImage : IDisposable /// /// The name of the file to save the image to. /// The format to save the image in. - /// The quality of the saved image. void Save(string filename, EncodeFormat format); /// @@ -71,7 +70,6 @@ public interface IImage : IDisposable /// /// The stream to save the image to. /// The format to save the image in. - /// The quality of the saved image. void Save(Stream stream, EncodeFormat format); /// @@ -79,7 +77,6 @@ public interface IImage : IDisposable /// /// The name of the file to save the image to. /// The format to save the image in. - /// The quality of the saved image. /// The cancellation token. /// A task representing the asynchronous save operation. Task SaveAsync(string filename, EncodeFormat format, CancellationToken token = default); @@ -89,7 +86,6 @@ public interface IImage : IDisposable /// /// The stream to save the image to. /// The format to save the image in. - /// The quality of the saved image. /// The cancellation token. /// A task representing the asynchronous save operation. Task SaveAsync(Stream stream, EncodeFormat format, CancellationToken token = default); From edbd44bbeef19107f6aec26e4794e19fe02a0453 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Thu, 24 Oct 2024 13:42:07 +0000 Subject: [PATCH 31/37] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 769e228f90..c360c4fb34 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net8.0 kavitareader.com Kavita - 0.8.3.16 + 0.8.3.17 en true From 1a88dd4fc0d307e2585ff5854e077c39a2dfc192 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Fri, 25 Oct 2024 09:22:12 -0700 Subject: [PATCH 32/37] Lots of Bugfixes (#3308) --- API.Tests/Services/ScannerServiceTests.cs | 138 ++++++---- .../Image Series with SP Folder - Manga.json | 6 + .../Series with Localized - Manga.json | 5 + API/Controllers/PersonController.cs | 5 +- API/Data/Repositories/PersonRepository.cs | 1 + .../QueryExtensions/Filtering/SeriesFilter.cs | 6 +- API/Services/ReadingItemService.cs | 3 +- API/Services/Tasks/Scanner/ProcessSeries.cs | 16 +- UI/Web/package-lock.json | 36 ++- UI/Web/src/app/_pipes/read-time-left.pipe.ts | 27 +- UI/Web/src/app/_services/action.service.ts | 4 +- UI/Web/src/app/_services/person.service.ts | 2 +- .../actionable-modal.component.html | 8 +- .../actionable-modal.component.ts | 43 ++-- .../card-actionables.component.ts | 2 +- .../related-tab/related-tab.component.html | 53 ++-- .../chapter-card/chapter-card.component.ts | 9 + .../cover-image-chooser.component.html | 6 +- .../series-card/series-card.component.ts | 16 +- .../volume-card/volume-card.component.ts | 10 +- .../chapter-detail.component.html | 2 +- .../chapter-detail.component.ts | 5 +- .../collection-detail.component.ts | 22 +- .../_components/dashboard.component.ts | 4 +- .../edit-person-modal.component.html | 1 + .../edit-person-modal.component.ts | 12 +- .../reading-list-detail.component.ts | 20 +- .../series-detail/series-detail.component.ts | 6 +- .../setting-item/setting-item.component.html | 2 +- .../setting-item/setting-item.component.ts | 1 + .../change-age-restriction.component.html | 4 +- .../change-age-restriction.component.scss | 3 + .../change-age-restriction.component.ts | 9 +- UI/Web/src/theme/themes/dark.scss | 1 - UI/Web/src/theme/themes/e-ink.scss | 188 -------------- UI/Web/src/theme/themes/light.scss | 243 ------------------ 36 files changed, 331 insertions(+), 588 deletions(-) create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json delete mode 100644 UI/Web/src/theme/themes/e-ink.scss delete mode 100644 UI/Web/src/theme/themes/light.scss diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index fcbfe82602..2a70ea79eb 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -7,7 +7,10 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; using API.Data; +using API.Data.Metadata; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; @@ -35,6 +38,7 @@ public class ScannerServiceTests : AbstractDbTest private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases"); private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png"); + private static readonly string[] ComicInfoExtensions = new[] { ".cbz", ".cbr", ".zip", ".rar" }; public ScannerServiceTests(ITestOutputHelper testOutputHelper) { @@ -125,9 +129,61 @@ public async Task ScanLibrary_FlatSeriesWithSpecial() Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null)); } - private async Task GenerateScannerData(string testcase) + /// + /// This is testing that if the first file is named A and has a localized name of B if all other files are named B, it should still group and name the series A + /// + [Fact] + public async Task ScanLibrary_LocalizedSeries() + { + const string testcase = "Series with Localized - Manga.json"; + + // Get the first file and generate a ComicInfo + var infos = new Dictionary(); + infos.Add("My Dress-Up Darling v01.cbz", new ComicInfo() + { + Series = "My Dress-Up Darling", + LocalizedSeries = "Sono Bisque Doll wa Koi wo Suru" + }); + + var library = await GenerateScannerData(testcase, infos); + + + var scanner = CreateServices(); + await scanner.ScanLibrary(library.Id); + var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + Assert.NotNull(postLib); + Assert.Single(postLib.Series); + Assert.Equal(3, postLib.Series.First().Volumes.Count); + } + + + /// + /// Files under a folder with a SP marker should group into one issue + /// + /// https://github.com/Kareadita/Kavita/issues/3299 + [Fact] + public async Task ScanLibrary_ImageSeries_SpecialGrouping() + { + const string testcase = "Image Series with SP Folder - Manga.json"; + + var library = await GenerateScannerData(testcase); + + + var scanner = CreateServices(); + await scanner.ScanLibrary(library.Id); + var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + Assert.NotNull(postLib); + Assert.Single(postLib.Series); + Assert.Equal(3, postLib.Series.First().Volumes.Count); + } + + + #region Setup + private async Task GenerateScannerData(string testcase, Dictionary comicInfos = null) { - var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase)); + var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos); var (publisher, type) = SplitPublisherAndLibraryType(Path.GetFileNameWithoutExtension(testcase)); @@ -148,11 +204,17 @@ private async Task GenerateScannerData(string testcase) private ScannerService CreateServices() { - var ds = new DirectoryService(Substitute.For>(), new FileSystem()); - var mockReadingService = new MockReadingItemService(ds, Substitute.For()); + var fs = new FileSystem(); + var ds = new DirectoryService(Substitute.For>(), fs); + var archiveService = new ArchiveService(Substitute.For>(), ds, + Substitute.For(), Substitute.For()); + var readingItemService = new ReadingItemService(archiveService, Substitute.For(), + Substitute.For(), ds, Substitute.For>()); + + var processSeries = new ProcessSeries(_unitOfWork, Substitute.For>(), Substitute.For(), - ds, Substitute.For(), mockReadingService, Substitute.For(), + ds, Substitute.For(), readingItemService, new FileService(fs), Substitute.For(), Substitute.For(), Substitute.For(), @@ -161,7 +223,7 @@ private ScannerService CreateServices() var scanner = new ScannerService(_unitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), Substitute.For(), ds, - mockReadingService, processSeries, Substitute.For()); + readingItemService, processSeries, Substitute.For()); return scanner; } @@ -189,7 +251,7 @@ private static (string Publisher, LibraryType Type) SplitPublisherAndLibraryType - private async Task GenerateTestDirectory(string mapPath) + private async Task GenerateTestDirectory(string mapPath, Dictionary comicInfos = null) { // Read the map file var mapContent = await File.ReadAllTextAsync(mapPath); @@ -206,7 +268,7 @@ private async Task GenerateTestDirectory(string mapPath) Directory.CreateDirectory(testDirectory); // Generate the files and folders - await Scaffold(testDirectory, filePaths); + await Scaffold(testDirectory, filePaths, comicInfos); _testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}"); @@ -214,7 +276,7 @@ private async Task GenerateTestDirectory(string mapPath) } - private async Task Scaffold(string testDirectory, List filePaths) + private async Task Scaffold(string testDirectory, List filePaths, Dictionary comicInfos = null) { foreach (var relativePath in filePaths) { @@ -229,9 +291,9 @@ private async Task Scaffold(string testDirectory, List filePaths) } var ext = Path.GetExtension(fullPath).ToLower(); - if (new[] { ".cbz", ".cbr", ".zip", ".rar" }.Contains(ext)) + if (ComicInfoExtensions.Contains(ext) && comicInfos != null && comicInfos.TryGetValue(Path.GetFileName(relativePath), out var info)) { - CreateMinimalCbz(fullPath, includeMetadata: true); + CreateMinimalCbz(fullPath, info); } else { @@ -242,54 +304,44 @@ private async Task Scaffold(string testDirectory, List filePaths) } } - private void CreateMinimalCbz(string filePath, bool includeMetadata) + private void CreateMinimalCbz(string filePath, ComicInfo? comicInfo = null) { - var tempImagePath = _imagePath; // Assuming _imagePath is a valid path to the 1x1 image - using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Create)) { // Add the 1x1 image to the archive - archive.CreateEntryFromFile(tempImagePath, "1x1.png"); + archive.CreateEntryFromFile(_imagePath, "1x1.png"); - if (includeMetadata) + if (comicInfo != null) { - var comicInfo = GenerateComicInfo(); + // Serialize ComicInfo object to XML + var comicInfoXml = SerializeComicInfoToXml(comicInfo); + + // Create an entry for ComicInfo.xml in the archive var entry = archive.CreateEntry("ComicInfo.xml"); using var entryStream = entry.Open(); using var writer = new StreamWriter(entryStream, Encoding.UTF8); - writer.Write(comicInfo); + + // Write the XML to the archive + writer.Write(comicInfoXml); } + } - Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(includeMetadata ? "" : "out")} metadata."); + Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata."); } - private string GenerateComicInfo() - { - var comicInfo = new StringBuilder(); - comicInfo.AppendLine(""); - comicInfo.AppendLine(""); - - // People Tags - string[] people = { "Joe Shmo", "Tommy Two Hands"}; - string[] genres = { /* Your list of genres here */ }; - - void AddRandomTag(string tagName, string[] choices) - { - if (new Random().Next(0, 2) == 1) // 50% chance to include the tag - { - var selected = choices.OrderBy(x => Guid.NewGuid()).Take(new Random().Next(1, 5)).ToArray(); - comicInfo.AppendLine($" <{tagName}>{string.Join(", ", selected)}"); - } - } - foreach (var tag in new[] { "Writer", "Penciller", "Inker", "CoverArtist", "Publisher", "Character", "Imprint", "Colorist", "Letterer", "Editor", "Translator", "Team", "Location" }) + private static string SerializeComicInfoToXml(ComicInfo comicInfo) + { + var xmlSerializer = new XmlSerializer(typeof(ComicInfo)); + using var stringWriter = new StringWriter(); + using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false), OmitXmlDeclaration = false})) { - AddRandomTag(tag, people); + xmlSerializer.Serialize(xmlWriter, comicInfo); } - AddRandomTag("Genre", genres); - comicInfo.AppendLine(""); - - return comicInfo.ToString(); + // For the love of god, I spent 2 hours trying to get utf-8 with no BOM + return stringWriter.ToString().Replace("""""", + @""); } + #endregion } diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json new file mode 100644 index 0000000000..62106703c7 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json @@ -0,0 +1,6 @@ +[ + "My Dress-Up Darling/My Dress-Up Darling vol 1/0001.png", + "My Dress-Up Darling/My Dress-Up Darling vol 1/0002.png", + "My Dress-Up Darling/My Dress-Up Darling vol 2/0001.png", + "My Dress-Up Darling/Specials/My Dress-Up Darling SP01/0001.png" +] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json new file mode 100644 index 0000000000..6495c294f0 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json @@ -0,0 +1,5 @@ +[ + "My Dress-Up Darling/My Dress-Up Darling v01.cbz", + "My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru v02.cbz", + "My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru ch 10.cbz" +] \ No newline at end of file diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs index fb18156bab..e5b9a99cce 100644 --- a/API/Controllers/PersonController.cs +++ b/API/Controllers/PersonController.cs @@ -11,6 +11,7 @@ using Nager.ArticleNumber; namespace API.Controllers; +#nullable enable public class PersonController : BaseApiController { @@ -39,11 +40,11 @@ public async Task>> GetRolesForPersonByName } /// - /// Returns a list of authors for browsing + /// Returns a list of authors & artists for browsing /// /// /// - [HttpPost("authors")] + [HttpPost("all")] public async Task>> GetAuthorsForBrowse([FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index a6bb7b2717..c6c4371033 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -171,6 +171,7 @@ public async Task> GetAllWritersAndSeriesCount(int us Id = p.Id, Name = p.Name, Description = p.Description, + CoverImage = p.CoverImage, SeriesCount = p.SeriesMetadataPeople .Where(smp => roles.Contains(smp.Role)) .Select(smp => smp.SeriesMetadata.SeriesId) diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index b6f1082af8..a0f88c582b 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -424,7 +424,7 @@ public static IQueryable HasReadingDate(this IQueryable queryabl public static IQueryable HasTags(this IQueryable queryable, bool condition, FilterComparison comparison, IList tags) { - if (!condition || tags.Count == 0) return queryable; + if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable; switch (comparison) { @@ -547,7 +547,7 @@ public static IQueryable HasPeopleLegacy(this IQueryable queryab public static IQueryable HasGenre(this IQueryable queryable, bool condition, FilterComparison comparison, IList genres) { - if (!condition || genres.Count == 0) return queryable; + if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable; switch (comparison) { @@ -620,7 +620,7 @@ public static IQueryable HasFormat(this IQueryable queryable, bo public static IQueryable HasCollectionTags(this IQueryable queryable, bool condition, FilterComparison comparison, IList collectionTags, IList collectionSeries) { - if (!condition || collectionTags.Count == 0) return queryable; + if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable; switch (comparison) diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index 34360efa54..3898bd2388 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -9,7 +9,6 @@ namespace API.Services; public interface IReadingItemService { - ComicInfo? GetComicInfo(string filePath); int GetNumberOfPages(string filePath, MangaFormat format); string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); @@ -51,7 +50,7 @@ public ReadingItemService(IArchiveService archiveService, IBookService bookServi /// /// Fully qualified path of file /// - public ComicInfo? GetComicInfo(string filePath) + private ComicInfo? GetComicInfo(string filePath) { if (Parser.IsEpub(filePath)) { diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index b5b0ffbcfa..dfc8dcda74 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -110,7 +110,7 @@ await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(firstInfo.Series, firs try { - _logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName); + _logger.LogInformation("[ScannerService] Processing series {SeriesName} with {Count} files", series.OriginalName, parsedInfos.Count); // parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort) var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, firstInfo); @@ -423,7 +423,7 @@ private async Task UpdateCollectionTags(Series series, Chapter firstChapter) var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(AppUserIncludes.Collections); if (defaultAdmin == null) return; - _logger.LogDebug("Collection tag(s) found for {SeriesName}, updating collections", series.Name); + _logger.LogInformation("Collection tag(s) found for {SeriesName}, updating collections", series.Name); var sw = Stopwatch.StartNew(); foreach (var collection in firstChapter.SeriesGroup.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) @@ -593,7 +593,6 @@ private async Task UpdateVolumes(Series series, IList parsedInfos, b { // Add new volumes and update chapters per volume var distinctVolumes = parsedInfos.DistinctVolumes(); - _logger.LogTrace("[ScannerService] Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name); foreach (var volumeNumber in distinctVolumes) { Volume? volume; @@ -621,7 +620,6 @@ private async Task UpdateVolumes(Series series, IList parsedInfos, b volume.LookupName = volumeNumber; volume.Name = volume.GetNumberTitle(); - _logger.LogTrace("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name); var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray(); await UpdateChapters(series, volume, infos, forceUpdate); @@ -641,7 +639,7 @@ private void RemoveVolumes(Series series, IList parsedInfos) if (series.Volumes.Count == nonDeletedVolumes.Count) return; - _logger.LogTrace("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", + _logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", (series.Volumes.Count - nonDeletedVolumes.Count), series.Name); var deletedVolumes = series.Volumes.Except(nonDeletedVolumes); foreach (var volume in deletedVolumes) @@ -655,7 +653,7 @@ private void RemoveVolumes(Series series, IList parsedInfos) file); } - _logger.LogTrace("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file); + _logger.LogDebug("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file); } series.Volumes = nonDeletedVolumes; @@ -681,7 +679,7 @@ private async Task UpdateChapters(Series series, Volume volume, IList parsedInfos) // If no files remain after filtering, remove the chapter if (existingChapter.Files.Count != 0) continue; - _logger.LogTrace("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", + _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series); volume.Chapters.Remove(existingChapter); } @@ -789,7 +787,7 @@ private void RemoveChapters(Volume volume, IList parsedInfos) // If no files exist, remove the chapter if (filesExist) continue; - _logger.LogTrace("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName} as no files exist", + _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName} as no files exist", existingChapter.Range, volume.Name, parsedInfos[0].Series); volume.Chapters.Remove(existingChapter); } diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 5c4a5c95be..09af58f198 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -504,6 +504,7 @@ "version": "17.3.4", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.4.tgz", "integrity": "sha512-TVWjpZSI/GIXTYsmVgEKYjBckcW8Aj62DcxLNehRFR+c7UB95OY3ZFjU8U4jL0XvWPgTkkVWQVq+P6N4KCBsyw==", + "dev": true, "dependencies": { "@babel/core": "7.23.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -531,6 +532,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -559,12 +561,14 @@ "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -745,6 +749,7 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -773,12 +778,14 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -5622,6 +5629,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -5634,6 +5642,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -5905,6 +5914,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "engines": { "node": ">=8" }, @@ -6216,6 +6226,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -6507,7 +6518,8 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/cookie": { "version": "0.6.0", @@ -7409,6 +7421,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -7418,6 +7431,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -8526,6 +8540,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -9207,6 +9222,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -11047,6 +11063,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -12436,6 +12453,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -12447,6 +12465,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -12457,7 +12476,8 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true }, "node_modules/regenerate": { "version": "1.4.2", @@ -12925,7 +12945,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "dev": true }, "node_modules/sass": { "version": "1.71.1", @@ -13044,6 +13064,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -13058,6 +13079,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -13068,7 +13090,8 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/send": { "version": "0.18.0", @@ -14199,6 +14222,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/UI/Web/src/app/_pipes/read-time-left.pipe.ts b/UI/Web/src/app/_pipes/read-time-left.pipe.ts index 7ac093dd7c..43ac41c868 100644 --- a/UI/Web/src/app/_pipes/read-time-left.pipe.ts +++ b/UI/Web/src/app/_pipes/read-time-left.pipe.ts @@ -1,6 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import {TranslocoService} from "@jsverse/transloco"; import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range"; +import {DecimalPipe} from "@angular/common"; @Pipe({ name: 'readTimeLeft', @@ -8,9 +9,31 @@ import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range"; }) export class ReadTimeLeftPipe implements PipeTransform { - constructor(private translocoService: TranslocoService) {} + constructor(private readonly translocoService: TranslocoService) {} transform(readingTimeLeft: HourEstimateRange): string { - return `~${readingTimeLeft.avgHours} ${readingTimeLeft.avgHours > 1 ? this.translocoService.translate('read-time-pipe.hours') : this.translocoService.translate('read-time-pipe.hour')}`; + const hoursLabel = readingTimeLeft.avgHours > 1 + ? this.translocoService.translate('read-time-pipe.hours') + : this.translocoService.translate('read-time-pipe.hour'); + + const formattedHours = this.customRound(readingTimeLeft.avgHours); + + return `~${formattedHours} ${hoursLabel}`; + } + + private customRound(value: number): string { + const integerPart = Math.floor(value); + const decimalPart = value - integerPart; + + if (decimalPart < 0.5) { + // Round down to the nearest whole number + return integerPart.toString(); + } else if (decimalPart >= 0.5 && decimalPart < 0.9) { + // Return with 1 decimal place + return value.toFixed(1); + } else { + // Round up to the nearest whole number + return Math.ceil(value).toString(); + } } } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 5ccdf9b227..4933ca0921 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -490,7 +490,7 @@ export class ActionService { this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id); this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id); - this.readingListModalRef.componentInstance.title = translate('action.multiple-selections'); + this.readingListModalRef.componentInstance.title = translate('actionable.multiple-selections'); this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple; @@ -530,7 +530,7 @@ export class ActionService { if (this.readingListModalRef != null) { return; } this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); this.readingListModalRef.componentInstance.seriesIds = series.map(v => v.id); - this.readingListModalRef.componentInstance.title = translate('action.multiple-selections'); + this.readingListModalRef.componentInstance.title = translate('actionable.multiple-selections'); this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series; diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index cc83e2a50e..006909d3eb 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -44,7 +44,7 @@ export class PersonService { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - return this.httpClient.post>(this.baseUrl + 'person/authors', {}, {observe: 'response', params}).pipe( + return this.httpClient.post>(this.baseUrl + 'person/all', {}, {observe: 'response', params}).pipe( map((response: any) => { return this.utilityService.createPaginatedResult(response) as PaginatedResult; }) diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html index 600e46637f..067dc5fb2b 100644 --- a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html +++ b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html @@ -2,21 +2,23 @@