Skip to content

Commit

Permalink
[Canary] Transcoding Support (#3489)
Browse files Browse the repository at this point in the history
Co-authored-by: Maximo Piva <mpiva@adventintermodal.com>
Co-authored-by: Maximo Piva <maximo.piva@gmail.com>
  • Loading branch information
3 people authored Jan 4, 2025
1 parent d880c16 commit 6cba7d1
Show file tree
Hide file tree
Showing 39 changed files with 1,638 additions and 420 deletions.
6 changes: 6 additions & 0 deletions API.Benchmark/API.Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.14.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
Expand All @@ -26,5 +27,10 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="Data\comic-normal.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
61 changes: 22 additions & 39 deletions API.Benchmark/ArchiveServiceBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
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 NSubstitute;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;


namespace API.Benchmark;

Expand All @@ -24,16 +24,16 @@ 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";
private readonly IImageFactory _imageFactory;
private const string SourceImage = "Data/comic-normal.jpg";


public ArchiveServiceBenchmark()
{
_directoryService = new DirectoryService(null, new FileSystem());
_imageService = new ImageService(null, _directoryService);
_imageService = new ImageService(null, _directoryService, Substitute.For<IImageFactory>());
_archiveService = new ArchiveService(new NullLogger<ArchiveService>(), _directoryService, _imageService, Substitute.For<IMediaErrorService>());
_imageFactory = new ImageMagickImageFactory();
}

[Benchmark(Baseline = true)]
Expand Down Expand Up @@ -61,50 +61,33 @@ public void TestGetComicInfo_outside_root()
}

[Benchmark]
public void ImageSharp_ExtractImage_PNG()
{
var outputDirectory = "C:/Users/josep/Pictures/imagesharp/";
_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);
}

[Benchmark]
public void ImageSharp_ExtractImage_WebP()
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);
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 = _imageFactory.Create(stream);
uint width = 320;
uint height = (uint)(thumbnail2.Height * (width / (double)thumbnail2.Width));
thumbnail2.Thumbnail(width, height);
thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.png"), EncodeFormat.PNG);
}

[Benchmark]
public void NetVips_ExtractImage_PNG()
public void ImageMagick_ExtractImage_WebP()
{
var outputDirectory = "C:/Users/josep/Pictures/netvips/";
var outputDirectory = "Data/ImageMagick";
_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 thumbnail2 = _imageFactory.Create(stream);
uint width = 320;
uint height = (uint)(thumbnail2.Height * (width / (double)thumbnail2.Width));
thumbnail2.Thumbnail(width, height);
thumbnail2.Save(_directoryService.FileSystem.Path.Join(outputDirectory, "imagesharp.webp"), EncodeFormat.PNG);
}

[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 = NetVips.Image.ThumbnailStream(stream, 320);
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, "netvips.webp"));
}

// Benchmark to test default GetNumberOfPages from archive
// vs a new method where I try to open the archive and return said stream
Expand Down
Binary file added API.Benchmark/Data/comic-normal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions API.Tests/API.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.1.3" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.3" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.1.7" />
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.7" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
17 changes: 12 additions & 5 deletions API.Tests/Services/ArchiveServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
using API.Archive;
using API.Entities.Enums;
using API.Services;
using API.Services.ImageServices;
using API.Services.ImageServices.ImageMagick;
using EasyCaching.Core;
using Microsoft.Extensions.Logging;
using NetVips;
using NSubstitute;
using NSubstitute.Extensions;
using Xunit;
Expand All @@ -29,7 +30,7 @@ public ArchiveServiceTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
_archiveService = new ArchiveService(_logger, _directoryService,
new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService),
new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService, Substitute.For<IImageFactory>()),
Substitute.For<IMediaErrorService>());
}

Expand Down Expand Up @@ -167,11 +168,17 @@ public void FindFirstEntry(string[] files, string expected)
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
{
var ds = Substitute.For<DirectoryService>(_directoryServiceLogger, new FileSystem());
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), ds);
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), ds, new ImageMagickImageFactory());
var archiveService = Substitute.For<ArchiveService>(_logger, ds, imageService, Substitute.For<IMediaErrorService>());

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 = imageService.ImageFactory.Create(Path.Join(testDirectory, expectedOutputFile));
uint width = 320;
uint height = (uint)(thumbnail.Height * (width / (double)thumbnail.Width));
thumbnail.Thumbnail(width, height);
using MemoryStream stream = new MemoryStream();
thumbnail.Save(stream, EncodeFormat.PNG);
var expectedBytes = stream.ToArray();

archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default);

Expand All @@ -198,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<ILogger<ImageService>>(), _directoryService);
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService, Substitute.For<IImageFactory>());
var archiveService = Substitute.For<ArchiveService>(_logger,
new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService,
Substitute.For<IMediaErrorService>());
Expand Down
3 changes: 2 additions & 1 deletion API.Tests/Services/BookServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.IO;
using System.IO.Abstractions;
using API.Services;
using API.Services.ImageServices;
using EasyCaching.Core;
using Microsoft.Extensions.Logging;
using NSubstitute;
Expand All @@ -17,7 +18,7 @@ public BookServiceTests()
{
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());
_bookService = new BookService(_logger, directoryService,
new ImageService(Substitute.For<ILogger<ImageService>>(), directoryService)
new ImageService(Substitute.For<ILogger<ImageService>>(), directoryService, Substitute.For<IImageFactory>())
, Substitute.For<IMediaErrorService>());
}

Expand Down
14 changes: 7 additions & 7 deletions API.Tests/Services/CacheServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public async Task Ensure_DirectoryAlreadyExists_DontExtractAnything()
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(),
Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(), Substitute.For<IImageService>());

await ResetDB();
var s = new SeriesBuilder("Test").Build();
Expand Down Expand Up @@ -234,7 +234,7 @@ public void CleanupChapters_AllFilesShouldBeDeleted()
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(), Substitute.For<IImageService>());

cleanupService.CleanupChapters(new []{1, 3});
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories));
Expand All @@ -256,7 +256,7 @@ public void GetCachedEpubFile_ShouldReturnFirstEpub()
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(), Substitute.For<IImageService>());

var c = new ChapterBuilder("1")
.WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build())
Expand Down Expand Up @@ -297,7 +297,7 @@ public void GetCachedPagePath_ReturnNullIfNoFiles()
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(),Substitute.For<IImageService>());

// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
Expand Down Expand Up @@ -341,7 +341,7 @@ public void GetCachedPagePath_GetFileFromFirstFile()
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(), Substitute.For<IImageService>());

// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
Expand Down Expand Up @@ -382,7 +382,7 @@ public void GetCachedPagePath_GetLastPageFromSingleFile()
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(), Substitute.For<IImageService>());

// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
Expand Down Expand Up @@ -427,7 +427,7 @@ public void GetCachedPagePath_GetFileFromSecondFile()
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
Substitute.For<IBookmarkService>(), Substitute.For<IImageService>());

// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
Expand Down
33 changes: 16 additions & 17 deletions API.Tests/Services/ImageServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
using System.Text;
using API.Entities.Enums;
using API.Services;
using API.Services.ImageServices.ImageMagick;
using EasyCaching.Core;
using Microsoft.Extensions.Logging;
using NetVips;
using NSubstitute;
using Xunit;
using Image = NetVips.Image;

namespace API.Tests.Services;

Expand Down Expand Up @@ -56,21 +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();
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 = Image.Thumbnail(imagePath, dims.Width, dims.Height,
size: size,
crop: crop);

var thumbnail = factory.Create(imagePath);
thumbnail = ImageService.Thumbnail(thumbnail, dims.Width, dims.Height);
var outputFileName = fileName + outputExtension + ".png";
thumbnail.WriteToFile(Path.Join(_testDirectory, outputFileName));
thumbnail.Save(Path.Join(_testDirectory, outputFileName), EncodeFormat.PNG);
}
}

Expand All @@ -80,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("<!DOCTYPE html>");
Expand All @@ -105,7 +99,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 = factory.Create(imagePath);
htmlBuilder.AppendLine("<div class=\"image-row\">");
htmlBuilder.AppendLine($"<p>{fileName} ({((double) sourceImage.Width / sourceImage.Height).ToString("F2")}) - {ImageService.WillScaleWell(sourceImage, dims.Width, dims.Height)}</p>");
htmlBuilder.AppendLine($"<img src=\"./{Path.GetFileName(imagePath)}\" alt=\"{fileName}\">");
Expand Down Expand Up @@ -144,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>();

ImageService service = new ImageService(logger, 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"));
Expand All @@ -164,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 = 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 = factory.Create(200,100,color.R, color.G, color.B);
colorImage.Save(outputPath, EncodeFormat.PNG);
}

private void GenerateHtmlFileForColorScape()
Expand Down
Loading

0 comments on commit 6cba7d1

Please sign in to comment.