Memory Leak Assistance Request #2952
-
I encountered a memory leak issue during my usage. A significant amount of unmanaged memory was not being released when creating thumbnails and converting image formats. I am a beginner in dotnet and would appreciate your assistance, thank you Below is my code: using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using System.Globalization;
using Foxel.Models;
using Foxel.Models.Enums;
using SixLabors.ImageSharp.PixelFormats;
namespace Foxel.Utils;
/// <summary>
/// Utility class for image processing
/// </summary>
public static class ImageHelper
{
/// <summary>
/// Gets the full URL path
/// </summary>
/// <param name="serverUrl">Server URL</param>
/// <param name="relativePath">Relative path</param>
/// <returns>Full URL path</returns>
public static string GetFullPath(string serverUrl, string relativePath)
{
if (string.IsNullOrEmpty(relativePath))
return string.Empty;
if (relativePath.StartsWith("https://"))
return relativePath;
return $"{serverUrl.TrimEnd('/')}{relativePath}";
}
/// <summary>
/// Creates a thumbnail
/// </summary>
/// <param name="originalPath">Original image path</param>
/// <param name="thumbnailPath">Thumbnail save path</param>
/// <param name="maxWidth">Maximum thumbnail width</param>
/// <param name="quality">Compression quality (1-100)</param>
/// <returns>File size of the generated thumbnail (in bytes)</returns>
public static async Task<long> CreateThumbnailAsync(string originalPath, string thumbnailPath, int maxWidth,
int quality = 75)
{
// Get original file size
var originalFileInfo = new FileInfo(originalPath);
long originalSize = originalFileInfo.Length;
using var image = await Image.LoadAsync(originalPath);
image.Metadata.ExifProfile = null;
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxWidth, 0),
Mode = ResizeMode.Max
}));
string webpThumbnailPath = Path.ChangeExtension(thumbnailPath, ".webp");
int adjustedQuality = AdjustQualityByFileSize(originalSize, ".webp", quality);
await image.SaveAsWebpAsync(webpThumbnailPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder
{
Quality = adjustedQuality,
Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality
});
var thumbnailFileInfo = new FileInfo(webpThumbnailPath);
if (thumbnailFileInfo.Length < originalSize) return thumbnailFileInfo.Length;
await image.SaveAsWebpAsync(webpThumbnailPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder
{
Quality = Math.Max(adjustedQuality - 15, 50),
Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality
});
thumbnailFileInfo = new FileInfo(webpThumbnailPath);
return thumbnailFileInfo.Length;
}
/// <summary>
/// Checks if the image contains transparent pixels
/// </summary>
/// <param name="image">Image to check</param>
/// <returns>True if the image contains transparent pixels</returns>
private static bool HasTransparency(Image image)
{
// Check if the image format supports transparency
if (image.PixelType.AlphaRepresentation == PixelAlphaRepresentation.None)
{
return false; // Image format does not support transparency
}
// For small images, check each pixel for transparency
if (image.Width * image.Height <= 1000 * 1000) // For images up to 1000x1000
{
using var imageWithAlpha = image.CloneAs<Rgba32>();
for (int y = 0; y < imageWithAlpha.Height; y++)
{
for (int x = 0; x < imageWithAlpha.Width; x++)
{
if (imageWithAlpha[x, y].A < 255)
{
return true;
}
}
}
return false;
}
else
{
using var imageWithAlpha = image.CloneAs<Rgba32>();
int sampleSize = Math.Max(image.Width, image.Height) / 100;
sampleSize = Math.Max(1, sampleSize);
for (int y = 0; y < imageWithAlpha.Height; y += sampleSize)
{
for (int x = 0; x < imageWithAlpha.Width; x += sampleSize)
{
if (imageWithAlpha[x, y].A < 255)
{
return true;
}
}
}
return false;
}
}
/// <summary>
/// Adjusts quality parameter based on original file size
/// </summary>
private static int AdjustQualityByFileSize(long originalSize, string extension, int baseQuality)
{
if (extension == ".webp")
{
if (originalSize > 10 * 1024 * 1024) // 10MB
return Math.Min(baseQuality, 70);
else if (originalSize > 5 * 1024 * 1024) // 5MB
return Math.Min(baseQuality, 75);
else if (originalSize > 1 * 1024 * 1024) // 1MB
return Math.Min(baseQuality, 80);
}
else if (extension == ".jpg" || extension == ".jpeg")
{
if (originalSize > 10 * 1024 * 1024) // 10MB
return Math.Min(baseQuality, 65);
else if (originalSize > 5 * 1024 * 1024) // 5MB
return Math.Min(baseQuality, 70);
else if (originalSize > 1 * 1024 * 1024) // 1MB
return Math.Min(baseQuality, 75);
}
return baseQuality;
}
/// <summary>
/// Converts an image to Base64 encoding
/// </summary>
/// <param name="imagePath">Image path</param>
/// <returns>Base64 encoded string</returns>
public static async Task<string> ConvertImageToBase64(string imagePath)
{
byte[] imageBytes = await File.ReadAllBytesAsync(imagePath);
return Convert.ToBase64String(imageBytes);
}
/// <summary>
/// Extracts EXIF information from an image
/// </summary>
/// <param name="imagePath">Image path</param>
/// <returns>EXIF information object</returns>
public static async Task<ExifInfo> ExtractExifInfoAsync(string imagePath)
{
var exifInfo = new ExifInfo();
try
{
// Ensure file exists
if (!File.Exists(imagePath))
{
exifInfo.ErrorMessage = "Image file not found";
return exifInfo;
}
// Read EXIF information using ImageSharp
using var image = await Image.LoadAsync(imagePath);
var exifProfile = image.Metadata.ExifProfile;
// Add basic image information
exifInfo.Width = image.Width;
exifInfo.Height = image.Height;
if (exifProfile != null)
{
// Extract camera information
if (exifProfile.TryGetValue(ExifTag.Make, out var make))
exifInfo.CameraMaker = make.Value;
if (exifProfile.TryGetValue(ExifTag.Model, out var model))
exifInfo.CameraModel = model.Value;
if (exifProfile.TryGetValue(ExifTag.Software, out var software))
exifInfo.Software = software.Value;
// Extract shooting parameters
if (exifProfile.TryGetValue(ExifTag.ExposureTime, out var exposureTime))
exifInfo.ExposureTime = exposureTime.Value.ToString();
if (exifProfile.TryGetValue(ExifTag.FNumber, out var fNumber))
exifInfo.Aperture = $"f/{fNumber.Value}";
if (exifProfile.TryGetValue(ExifTag.ISOSpeedRatings, out var iso))
{
if (iso.Value is { Length: > 0 } isoArray)
{
exifInfo.IsoSpeed = isoArray[0].ToString();
}
else
{
exifInfo.IsoSpeed = iso.Value?.ToString();
}
}
if (exifProfile.TryGetValue(ExifTag.FocalLength, out var focalLength))
exifInfo.FocalLength = $"{focalLength.Value}mm";
if (exifProfile.TryGetValue(ExifTag.Flash, out var flash))
exifInfo.Flash = flash.Value.ToString();
if (exifProfile.TryGetValue(ExifTag.MeteringMode, out var meteringMode))
exifInfo.MeteringMode = meteringMode.Value.ToString();
if (exifProfile.TryGetValue(ExifTag.WhiteBalance, out var whiteBalance))
exifInfo.WhiteBalance = whiteBalance.Value.ToString();
// Extract time information and store as string
if (exifProfile.TryGetValue(ExifTag.DateTimeOriginal, out var dateTime))
{
exifInfo.DateTimeOriginal = dateTime.Value;
// Parse date time
if (DateTime.TryParseExact(dateTime.Value, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture,
DateTimeStyles.None, out _))
{
// Keep only original string format in ExifInfo
}
}
// Extract GPS information
if (exifProfile.TryGetValue(ExifTag.GPSLatitude, out var latitude) &&
exifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out var latitudeRef))
{
string? latRef = latitudeRef.Value;
exifInfo.GpsLatitude = ConvertGpsCoordinateToString(latitude.Value, latRef == "S");
}
if (exifProfile.TryGetValue(ExifTag.GPSLongitude, out var longitude) &&
exifProfile.TryGetValue(ExifTag.GPSLongitudeRef, out var longitudeRef))
{
string? longRef = longitudeRef.Value;
exifInfo.GpsLongitude = ConvertGpsCoordinateToString(longitude.Value, longRef == "W");
}
}
}
catch (Exception ex)
{
exifInfo.ErrorMessage = $"Error extracting EXIF information: {ex.Message}";
}
return exifInfo;
}
/// <summary>
/// Converts GPS coordinates to string representation
/// </summary>
/// <param name="rationals">Array of rational numbers for GPS coordinates (degrees, minutes, seconds)</param>
/// <param name="isNegative">Whether the coordinate is negative (South latitude/West longitude)</param>
/// <returns>Decimal format GPS coordinate</returns>
private static string? ConvertGpsCoordinateToString(Rational[]? rationals, bool isNegative)
{
if (rationals == null || rationals.Length < 3)
return null;
try
{
// Convert degrees, minutes, seconds to decimal degrees
double degrees = rationals[0].Numerator / (double)rationals[0].Denominator;
double minutes = rationals[1].Numerator / (double)rationals[1].Denominator;
double seconds = rationals[2].Numerator / (double)rationals[2].Denominator;
double coordinate = degrees + (minutes / 60) + (seconds / 3600);
// Negate if South latitude or West longitude
if (isNegative)
coordinate = -coordinate;
return coordinate.ToString(CultureInfo.InvariantCulture);
}
catch
{
return null;
}
}
/// <summary>
/// Parses the shooting time from EXIF information
/// </summary>
/// <param name="dateTimeOriginal">EXIF shooting time string</param>
/// <returns>UTC formatted date time, or null if parsing fails</returns>
public static DateTime? ParseExifDateTime(string? dateTimeOriginal)
{
if (string.IsNullOrEmpty(dateTimeOriginal))
return null;
if (DateTime.TryParseExact(dateTimeOriginal, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var parsedDate))
{
return DateTime.SpecifyKind(parsedDate, DateTimeKind.Local).ToUniversalTime();
}
return null;
}
/// <summary>
/// Converts image format (lossless conversion with EXIF preservation)
/// </summary>
/// <param name="inputPath">Input image path</param>
/// <param name="outputPath">Output image path</param>
/// <param name="targetFormat">Target format</param>
/// <param name="quality">Compression quality (for JPEG and WebP only, 1-100)</param>
/// <returns>Path of the converted file</returns>
public static async Task<string> ConvertImageFormatAsync(string inputPath, string outputPath, ImageFormat targetFormat, int quality = 95)
{
if (targetFormat == ImageFormat.Original)
{
// If original format, return input path
return inputPath;
}
using var image = await Image.LoadAsync(inputPath);
// Preserve original EXIF information
var originalExifProfile = image.Metadata.ExifProfile;
// Determine file extension and output path based on target format
string extension = GetFileExtensionFromFormat(targetFormat);
string finalOutputPath = Path.ChangeExtension(outputPath, extension);
switch (targetFormat)
{
case ImageFormat.Jpeg:
await image.SaveAsJpegAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
{
Quality = quality
});
break;
case ImageFormat.Png:
await image.SaveAsPngAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Png.PngEncoder
{
CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.BestCompression,
ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha
});
break;
case ImageFormat.WebP:
await image.SaveAsWebpAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder
{
Quality = quality,
Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality
});
break;
default:
throw new NotSupportedException($"Unsupported image format: {targetFormat}");
}
// If original image has EXIF information, save to converted image
if (originalExifProfile != null)
{
using var convertedImage = await Image.LoadAsync(finalOutputPath);
convertedImage.Metadata.ExifProfile = originalExifProfile;
switch (targetFormat)
{
case ImageFormat.Jpeg:
await convertedImage.SaveAsJpegAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
{
Quality = quality
});
break;
case ImageFormat.Png:
await convertedImage.SaveAsPngAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Png.PngEncoder
{
CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.BestCompression,
ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha
});
break;
case ImageFormat.WebP:
await convertedImage.SaveAsWebpAsync(finalOutputPath, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder
{
Quality = quality,
Method = SixLabors.ImageSharp.Formats.Webp.WebpEncodingMethod.BestQuality
});
break;
}
}
return finalOutputPath;
}
/// <summary>
/// Gets file extension based on image format
/// </summary>
/// <param name="format">Image format</param>
/// <returns>File extension</returns>
public static string GetFileExtensionFromFormat(ImageFormat format)
{
return format switch
{
ImageFormat.Jpeg => ".jpg",
ImageFormat.Png => ".png",
ImageFormat.WebP => ".webp",
_ => throw new NotSupportedException($"Unsupported image format: {format}")
};
}
/// <summary>
/// Gets MIME type based on image format
/// </summary>
/// <param name="format">Image format</param>
/// <returns>MIME type</returns>
public static string GetMimeTypeFromFormat(ImageFormat format)
{
return format switch
{
ImageFormat.Jpeg => "image/jpeg",
ImageFormat.Png => "image/png",
ImageFormat.WebP => "image/webp",
_ => throw new NotSupportedException($"Unsupported image format: {format}")
};
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
ImageSharp is pooling umanaged memory for performance reasons. In most cases there is no harm from this behavior and the pool sizes have a platform-specific upper limit, but there are knobs to customize it if necessary: https://docs.sixlabors.com/articles/imagesharp/memorymanagement.html Note that missing dispose calls / using blocks can make the pooling behavior suboptimal by delaying pool returns until finalizer runs. Your code seems to be correct at the first glance, but just in case, note that the quoted article demonstrates a technique to troubleshoot |
Beta Was this translation helpful? Give feedback.
ImageSharp is pooling umanaged memory for performance reasons. In most cases there is no harm from this behavior and the pool sizes have a platform-specific upper limit, but there are knobs to customize it if necessary: https://docs.sixlabors.com/articles/imagesharp/memorymanagement.html
Note that missing dispose calls / using blocks can make the pooling behavior suboptimal by delaying pool returns until finalizer runs. Your code seems to be correct at the first glance, but just in case, note that the quoted article demonstrates a technique to troubleshoot
Image
/buffer leaks.