Skip to content
Open
2 changes: 1 addition & 1 deletion API.Tests/Services/CoverDbServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public async Task DownloadFaviconAsync_ShouldDownloadAndMatchExpectedFavicon()

// Load and compare similarity

var similarity = _imageService.ImageFactory.CalculateSimilarity(expectedFaviconPath, actualFaviconPath); // Assuming you have this extension
var similarity = _imageService.CalculateSimilarity(expectedFaviconPath, actualFaviconPath); // Assuming you have this extension
Assert.True(similarity > 0.9f, $"Image similarity too low: {similarity}");
}

Expand Down
16 changes: 11 additions & 5 deletions API/Extensions/HttpExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ private static void AddExtension(List<string> extensions, string extension)
if (string.IsNullOrEmpty(extension))
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anytime you don't use {} on an if statement, it must be on one line and MUST be a jump operation (return/continue/break). All other times, you must use {} and be on 2 lines.

if (!extensions.Contains(extension))
{
extensions.Add(extension);
}
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method performs a linear search with Contains() for each extension. Consider using a HashSet for better performance when dealing with many extensions.

Suggested change
}
private static void AddExtension(HashSet<string> extensions, string extension)
{
if (string.IsNullOrEmpty(extension))
return;
extensions.Add(extension);

Copilot uses AI. Check for mistakes.
}

/// <summary>
Expand All @@ -71,15 +73,18 @@ private static void AddExtension(List<string> extensions, string extension)
/// <returns>A list of supported image types extensions by the Browser.</returns>
public static List<string> SupportedImageTypesFromRequest(this HttpRequest request)
{
// Add default extensions supported by all browsers.
List<string> supportedExtensions = Parser.UniversalFileImageExtensionArray.ToList();
//Early eject if the browser or api do not provide an Accept header.
if (!request.Headers.ContainsKey("Accept"))
return supportedExtensions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be on the same line as the if


var acceptHeader = request.Headers["Accept"].ToString();
var split = acceptHeader.Split(';');
var split = acceptHeader.Split(';'); //remove any parameters like "q=0.8"
acceptHeader = split[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like some safety checks in the crazy off chance we don't have anything in the header.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, will do

split = acceptHeader.Split(',');

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

// 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.
Expand All @@ -88,7 +93,8 @@ public static List<string> SupportedImageTypesFromRequest(this HttpRequest reque
if (v.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inversion of Control here please

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inversion of control means instead of doing if (condition) {}, inverting the condition, exist early then the body of the if doesn't need to be indented.

{
var mimeImagePart = v.Substring(6).ToLowerInvariant();
if (mimeImagePart.StartsWith("*")) continue;
if (mimeImagePart.StartsWith("*"))
continue;
if (Parser.NonUniversalSupportedMimeMappings.ContainsKey(mimeImagePart))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline above. Since there is a jump statement above, a newline helps bring that to the reader's attention.

{
Parser.NonUniversalSupportedMimeMappings[mimeImagePart].ForEach(x => AddExtension(supportedExtensions, x));
Expand Down
29 changes: 15 additions & 14 deletions API/Extensions/ImageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using API.Services;

namespace API.Extensions;

Expand Down Expand Up @@ -31,14 +32,14 @@ private sealed class ImageQualityMetrics
/// <param name="imagePath1">Path to first image</param>
/// <param name="imagePath2">Path to the second image</param>
/// <returns>Similarity score between 0-1, where 1 is identical</returns>
public static float CalculateSimilarity(this IImageFactory imageFactory, string imagePath1, string imagePath2)
public static float CalculateSimilarity(this IImageService imageService, string imagePath1, string imagePath2)
{
if (!File.Exists(imagePath1) || !File.Exists(imagePath2))
{
throw new FileNotFoundException("One or both image files do not exist");
}
using var im1 = imageFactory.Create(imagePath1);
using var im2 = imageFactory.Create(imagePath2);
using var im1 = imageService.ImageFactory.Create(imagePath1);
using var im2 = imageService.ImageFactory.Create(imagePath2);
var res1 = im1.Width * im1.Height;
var res2 = im2.Width * im2.Height;
var resolutionDiff = Math.Abs(res1 - res2) / (float)Math.Max(res1, res2);
Expand Down Expand Up @@ -89,16 +90,16 @@ public static float GetMeanSquaredError(this float[] img1, float[] img2, int pix
/// <param name="imagePath2">Path to the second image</param>
/// <param name="preferColor">Whether to prefer color images over grayscale (default: true)</param>
/// <returns>The path of the better image</returns>
public static string GetBetterImage(this IImageFactory imageFactory, string imagePath1, string imagePath2, bool preferColor = true)
public static string GetBetterImage(this IImageService imageService, string imagePath1, string imagePath2, bool preferColor = true)
{
if (!File.Exists(imagePath1) || !File.Exists(imagePath2))
{
throw new FileNotFoundException("One or both image files do not exist");
}

// Quick metadata check to get width/height without loading full pixel data
var info1 = imageFactory.GetDimensions(imagePath1);
var info2 = imageFactory.GetDimensions(imagePath2);
var info1 = imageService.ImageFactory.GetDimensions(imagePath1);
var info2 = imageService.ImageFactory.GetDimensions(imagePath2);

// Calculate resolution factor
double resolutionFactor1 = info1.Value.Width * info1.Value.Height;
Expand All @@ -115,13 +116,13 @@ public static string GetBetterImage(this IImageFactory imageFactory, string imag

// NOTE: We HAVE to use these scope blocks and load image here otherwise memory-mapped section exception will occur
ImageQualityMetrics metrics1;
using (var img1 = imageFactory.Create(imagePath1))
using (var img1 = imageService.ImageFactory.Create(imagePath1))
{
metrics1 = GetImageQualityMetrics(img1);
}

ImageQualityMetrics metrics2;
using (var img2 = imageFactory.Create(imagePath2))
using (var img2 = imageService.ImageFactory.Create(imagePath2))
{
metrics2 = GetImageQualityMetrics(img2);
}
Expand Down Expand Up @@ -186,23 +187,23 @@ private static ImageQualityMetrics GetImageQualityMetrics(IImage image)

var metrics = new ImageQualityMetrics
{
Width = (int)image.Width,
Height = (int)image.Height
Width = image.Width,
Height = image.Height
};

// Color analysis (is the image color or grayscale?)
var colorInfo = AnalyzeColorfulness(workingImage, (int)image.Width, (int)image.Height);
var colorInfo = AnalyzeColorfulness(workingImage, image.Width, image.Height);
metrics.IsColor = colorInfo.IsColor;
metrics.Colorfulness = colorInfo.Colorfulness;

// Contrast analysis
metrics.Contrast = CalculateContrast(workingImage, (int)image.Width, (int)image.Height);
metrics.Contrast = CalculateContrast(workingImage, image.Width, image.Height);

// Sharpness estimation
metrics.Sharpness = EstimateSharpness(workingImage, (int)image.Width, (int)image.Height);
metrics.Sharpness = EstimateSharpness(workingImage, image.Width, image.Height);

// Noise estimation
metrics.NoiseLevel = EstimateNoiseLevel(workingImage, (int)image.Width, (int)image.Height);
metrics.NoiseLevel = EstimateNoiseLevel(workingImage, image.Width, image.Height);

// Clean up
image.Dispose();
Expand Down
4 changes: 2 additions & 2 deletions API/Services/CacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ public IEnumerable<FileDimensionDto> GetCachedFileDimensions(string cachePath)
dimensions.Add(new FileDimensionDto()
{
PageNumber = i,
Height = (int)info.Value.Height,
Width = (int)info.Value.Width,
Height = info.Value.Height,
Width = info.Value.Width,
IsWide = info.Value.Width > info.Value.Height,
FileName = file.Replace(cachePath, string.Empty)
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ public IImage Create(int width, int height, byte red = 0, byte green = 0, byte b
MagickImageInfo info = new MagickImageInfo(filename);
return ((int)info.Width, (int)info.Height);
}
catch (Exception e)
catch
{
// Ignore errors and return null
}
return null;
}
Expand All @@ -84,7 +85,9 @@ public List<Vector3> GetRgbPixelsPercentage(string filename, float percent)
// Convert to list of Vector3 (RGB)

for (uint x = 0; x < pixels.Length; x += 4)
{
rgbPixels.Add(new Vector3(pixels[x], pixels[x + 1], pixels[x + 2]));
}
return rgbPixels;
}
}
Expand Down
16 changes: 10 additions & 6 deletions API/Services/ImageServices/NetVips/NetVipsImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ public static IImage CreateFromBGRAByteArray(byte[] bgraByteArray, int width, in
const int bands = 4; // BGRA

if (bgraByteArray.Length != width * height * bands)
{
throw new ArgumentException("Input byte array size doesn't match BGRA dimensions.");

}
// Load raw BGRA image
var image = Image.NewFromMemory(bgraByteArray, width, height, bands, Enums.BandFormat.Uchar);

Expand Down Expand Up @@ -87,11 +88,11 @@ public static IImage CreateThumbnail(Stream stream, int width, int height)
{
var scalingSize = NetVipsImage.GetSizeForDimensions(image, width, height);
var scalingCrop = NetVipsImage.GetCropForDimensions(image, width, height);
using var thumbnail2 = Image.ThumbnailStream(stream, width, height: height,
size: scalingSize,
crop: scalingCrop);
NetVipsImage g = new NetVipsImage();
g._image = thumbnail2;
g._image = Image.ThumbnailStream(stream, width, height: height,
size: scalingSize,
crop: scalingCrop
);
return g;
}
}
Expand Down Expand Up @@ -164,7 +165,8 @@ public IImage Thumbnail(int width, int height)
NetVipsImage im = new NetVipsImage();
im._image = _image.ThumbnailImage(width, height: height,
size: GetSizeForDimensions(this, width, height),
crop: GetCropForDimensions(this, width, height));
crop: GetCropForDimensions(this, width, height)
);
return im;
}

Expand All @@ -173,7 +175,9 @@ public void Composite(IImage overlay, int x, int y)
{
NetVipsImage im = overlay as NetVipsImage;
if (im == null)
{
throw new ArgumentNullException(nameof(overlay));
}
_image = _image.Insert(im._image, x, y);
}

Expand Down
7 changes: 5 additions & 2 deletions API/Services/ImageServices/NetVips/NetVipsImageFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ public IImage Create(int width, int height, byte red = 0, byte green = 0, byte b
NetVipsImage image = new NetVipsImage(filename);
return (image.Width, image.Height);
}
catch (Exception)
catch
{
// Ignore errors and return null
}
return null;
}
Expand All @@ -67,10 +68,12 @@ public List<Vector3> GetRgbPixelsPercentage(string filename, float percent)
using var res = im.Resize(percent / 100f);
float[] pixels = NetVipsImage.GetRGBAFloatImageDataFromImage(res);
if (pixels == null)
return new List<Vector3>();
return [];
var rgbPixels = new List<Vector3>();
for (uint x = 0; x < pixels.Length; x += 4)
{
rgbPixels.Add(new Vector3(pixels[x], pixels[x + 1], pixels[x + 2]));
}
return rgbPixels;
}
}
Expand Down
8 changes: 4 additions & 4 deletions API/Services/Tasks/Metadata/CoverDbService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64
if (checkNoImagePlaceholder)
{
var placeholderPath = Path.Combine(_directoryService.AssetsDirectory, "anilist-no-image-placeholder.jpg");
var similarity = _imageService.ImageFactory.CalculateSimilarity(placeholderPath, tempFullPath);
var similarity = _imageService.CalculateSimilarity(placeholderPath, tempFullPath);
if (similarity >= 0.9f)
{
_logger.LogInformation("Skipped setting placeholder image for person {PersonId} due to high similarity ({Similarity})", person.Id, similarity);
Expand All @@ -491,7 +491,7 @@ public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64
if (!string.IsNullOrEmpty(person.CoverImage))
{
var existingPath = Path.Combine(_directoryService.CoverImageDirectory, person.CoverImage);
var betterImage = _imageService.ImageFactory.GetBetterImage(existingPath,tempFullPath)!;
var betterImage = _imageService.GetBetterImage(existingPath, tempFullPath)!;

var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase);
if (choseNewImage)
Expand Down Expand Up @@ -569,7 +569,7 @@ public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64
try
{
var existingPath = Path.Combine(_directoryService.CoverImageDirectory, series.CoverImage);
var betterImage =_imageService.ImageFactory.GetBetterImage(existingPath, tempFullPath)!;
var betterImage =_imageService.GetBetterImage(existingPath, tempFullPath)!;
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after the '=' operator. Should be: var betterImage = _imageService.GetBetterImage(existingPath, tempFullPath)!;

Copilot uses AI. Check for mistakes.

var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase);
if (choseNewImage)
Expand Down Expand Up @@ -646,7 +646,7 @@ public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBas
try
{
var existingPath = Path.Combine(_directoryService.CoverImageDirectory, chapter.CoverImage);
var betterImage = _imageService.ImageFactory.GetBetterImage(existingPath,tempFullPath)!;
var betterImage = _imageService.GetBetterImage(existingPath,tempFullPath)!;
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after comma. Should be: var betterImage = _imageService.GetBetterImage(existingPath, tempFullPath)!;

Copilot uses AI. Check for mistakes.
var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase);

if (choseNewImage)
Expand Down