Skip to content

Commit

Permalink
Requested changes.
Browse files Browse the repository at this point in the history
Added Per Image Lock on conversion, to prevent multiple threads from processing the same image at the same time.
  • Loading branch information
maxpiva committed Oct 10, 2024
1 parent 4f026e6 commit 00ee60b
Show file tree
Hide file tree
Showing 6 changed files with 1,096 additions and 45 deletions.
6 changes: 3 additions & 3 deletions API/Entities/Enums/EncodeFormat.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
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,
//Internal Use
/// Internal Use
[Description("JPEG")]
JPEG = 3
}
Expand Down
22 changes: 12 additions & 10 deletions API/Extensions/HttpExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,30 @@ public static List<string> SupportedImageTypesFromRequest(this HttpRequest reque

List<string> supportedExtensions = new List<string>();

//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;
}
}
84 changes: 57 additions & 27 deletions API/Services/ImageService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -155,19 +159,13 @@ public interface IImageService
public class ImageService : IImageService
{
public const string Name = "ImageService";
private readonly ILogger<ImageService> _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


/// <summary>
/// Width of the Thumbnail generation
/// </summary>
Expand All @@ -181,6 +179,10 @@ public class ImageService : IImageService
/// </summary>
public const int LibraryThumbnailWidth = 32;

private readonly ILogger<ImageService> _logger;
private readonly IDirectoryService _directoryService;
private readonly IEasyCachingProviderFactory _cacheFactory;
private readonly IImageFactory _imageFactory;

private static readonly string[] ValidIconRelations = {
"icon",
Expand All @@ -197,6 +199,21 @@ public class ImageService : IImageService
["https://app.plex.tv"] = "https://plex.tv"
};



private static NamedMonitor _lock = new NamedMonitor();
/// <summary>
/// Represents a named monitor that provides thread-safe access to objects based on their names.
/// </summary>
class NamedMonitor
{
readonly ConcurrentDictionary<string, object> _dictionary = new ConcurrentDictionary<string, object>();

public object this[string name] => _dictionary.GetOrAdd(name, _ => new object());
}



/// <summary>
/// Initializes a new instance of the <see cref="ImageService"/> class.
/// </summary>
Expand Down Expand Up @@ -229,7 +246,7 @@ public void ExtractImages(string? fileFilePath, string targetDirectory, int file
Tasks.Scanner.Parser.Parser.ImageFileExtensions);
}
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -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;
}
/// <inheritdoc/>
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default, int quality = 100)
Expand All @@ -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;
}
/// <inheritdoc/>
Expand All @@ -361,6 +377,7 @@ public async Task<string> 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;
}

Expand All @@ -369,9 +386,7 @@ public Task<bool> 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)
{
Expand Down Expand Up @@ -521,7 +536,7 @@ public async Task<string> DownloadPublisherImageAsync(string publisherName, Enco

return (null, null);
}

private static bool IsColorCloseToWhiteOrBlack(Vector3 color)
{
var (_, _, lightness) = RgbToHsl(color);
Expand Down Expand Up @@ -690,26 +705,41 @@ public ColorScape CalculateColorScape(string sourceFile)
}
private bool CheckDirectSupport(string filename, List<string> supportedImageFormats)
{
if (supportedImageFormats == null)
return false;
if (supportedImageFormats == null) return false;

string ext = Path.GetExtension(filename).ToLowerInvariant().Substring(1);
return supportedImageFormats.Contains(ext);
}


/// <inheritdoc/>
public string ReplaceImageFileFormat(string filename, List<string> 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;
}

Expand Down
Loading

0 comments on commit 00ee60b

Please sign in to comment.