using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using WorkCameraExport.Services.Interfaces;
using Image = SixLabors.ImageSharp.Image;
namespace WorkCameraExport.Services
{
///
/// 图片服务实现 - 负责图片下载和处理
///
public class ImageService : IImageService
{
private readonly HttpClient _httpClient;
private readonly ILogService? _logService;
private bool _disposed;
public ImageService(ILogService? logService = null)
{
_logService = logService;
_httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(60)
};
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("WorkCameraExport/2.0 (Windows; .NET)");
}
///
/// 用于测试的构造函数,允许注入 HttpClient
///
internal ImageService(HttpClient httpClient, ILogService? logService = null)
{
_httpClient = httpClient;
_logService = logService;
}
#region 下载功能
///
/// 下载单张图片
///
public async Task DownloadImageAsync(string url, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(url))
{
_logService?.Warn("下载图片失败: URL 为空");
return null;
}
try
{
using var response = await _httpClient.GetAsync(url, cancellationToken);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadAsByteArrayAsync(cancellationToken);
_logService?.Info($"下载图片成功: {url}, 大小: {data.Length} bytes");
return data;
}
_logService?.Warn($"下载图片失败: {url}, 状态码: {response.StatusCode}");
return null;
}
catch (OperationCanceledException)
{
_logService?.Info($"下载图片已取消: {url}");
return null;
}
catch (Exception ex)
{
_logService?.Error($"下载图片异常: {url}", ex);
return null;
}
}
///
/// 批量并发下载图片
/// 使用 SemaphoreSlim 控制并发数
///
public async Task> DownloadImagesAsync(
List urls,
string outputDir,
int concurrency,
IProgress? progress = null,
CancellationToken cancellationToken = default)
{
if (urls == null || urls.Count == 0)
{
return new Dictionary();
}
// 确保并发数在有效范围内
concurrency = Math.Clamp(concurrency, 1, 10);
// 确保输出目录存在
if (!Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
var results = new Dictionary();
var downloadedCount = 0;
var lockObject = new object();
var isCancelled = false;
// 使用 SemaphoreSlim 控制并发
using var semaphore = new SemaphoreSlim(concurrency, concurrency);
var tasks = urls.Select(async url =>
{
try
{
// 检查是否已取消
if (cancellationToken.IsCancellationRequested || isCancelled)
{
lock (lockObject)
{
if (!results.ContainsKey(url))
{
results[url] = null;
}
}
return;
}
await semaphore.WaitAsync(cancellationToken);
try
{
// 再次检查取消状态
if (cancellationToken.IsCancellationRequested)
{
lock (lockObject)
{
if (!results.ContainsKey(url))
{
results[url] = null;
}
}
return;
}
var localPath = await DownloadImageToFileAsync(url, outputDir, cancellationToken);
lock (lockObject)
{
results[url] = localPath;
downloadedCount++;
progress?.Report(downloadedCount);
}
}
finally
{
semaphore.Release();
}
}
catch (OperationCanceledException)
{
isCancelled = true;
lock (lockObject)
{
if (!results.ContainsKey(url))
{
results[url] = null;
}
}
}
}).ToList();
try
{
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
// 取消时忽略异常
}
_logService?.Info($"批量下载完成: 成功 {results.Count(r => r.Value != null)}/{urls.Count}");
return results;
}
///
/// 下载图片到文件
///
private async Task DownloadImageToFileAsync(
string url,
string outputDir,
CancellationToken cancellationToken)
{
try
{
var imageData = await DownloadImageAsync(url, cancellationToken);
if (imageData == null || imageData.Length == 0)
{
return null;
}
// 从 URL 提取文件名,如果无法提取则生成唯一文件名
var fileName = GetFileNameFromUrl(url);
var filePath = Path.Combine(outputDir, fileName);
// 确保文件名唯一
filePath = EnsureUniqueFilePath(filePath);
await File.WriteAllBytesAsync(filePath, imageData, cancellationToken);
return filePath;
}
catch (OperationCanceledException)
{
return null;
}
catch (Exception ex)
{
_logService?.Error($"保存图片到文件失败: {url}", ex);
return null;
}
}
///
/// 从 URL 提取文件名
///
private static string GetFileNameFromUrl(string url)
{
try
{
var uri = new Uri(url);
var fileName = Path.GetFileName(uri.LocalPath);
if (!string.IsNullOrWhiteSpace(fileName) && fileName.Contains('.'))
{
// 清理文件名中的非法字符
foreach (var c in Path.GetInvalidFileNameChars())
{
fileName = fileName.Replace(c, '_');
}
return fileName;
}
}
catch
{
// URL 解析失败
}
// 生成唯一文件名
return $"{Guid.NewGuid():N}.jpg";
}
///
/// 确保文件路径唯一
///
private static string EnsureUniqueFilePath(string filePath)
{
if (!File.Exists(filePath))
{
return filePath;
}
var directory = Path.GetDirectoryName(filePath) ?? "";
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
var extension = Path.GetExtension(filePath);
var counter = 1;
string newPath;
do
{
newPath = Path.Combine(directory, $"{fileNameWithoutExt}_{counter}{extension}");
counter++;
} while (File.Exists(newPath));
return newPath;
}
#endregion
#region 图片处理功能
///
/// 压缩图片
///
public byte[] CompressImage(byte[] imageData, int quality)
{
if (imageData == null || imageData.Length == 0)
{
throw new ArgumentException("图片数据不能为空", nameof(imageData));
}
// 确保质量在有效范围内
quality = Math.Clamp(quality, 1, 100);
try
{
using var inputStream = new MemoryStream(imageData);
using var image = Image.Load(inputStream);
using var outputStream = new MemoryStream();
var encoder = new JpegEncoder
{
Quality = quality
};
image.Save(outputStream, encoder);
return outputStream.ToArray();
}
catch (Exception ex)
{
_logService?.Error("压缩图片失败", ex);
throw;
}
}
///
/// 缩放图片
///
public byte[] ResizeImage(byte[] imageData, int width, int height)
{
if (imageData == null || imageData.Length == 0)
{
throw new ArgumentException("图片数据不能为空", nameof(imageData));
}
if (width <= 0 || height <= 0)
{
throw new ArgumentException("宽度和高度必须大于 0");
}
try
{
using var inputStream = new MemoryStream(imageData);
using var image = Image.Load(inputStream);
using var outputStream = new MemoryStream();
image.Mutate(x => x.Resize(width, height));
image.Save(outputStream, new JpegEncoder { Quality = 90 });
return outputStream.ToArray();
}
catch (Exception ex)
{
_logService?.Error($"缩放图片失败: {width}x{height}", ex);
throw;
}
}
///
/// 压缩并缩放图片
///
public byte[] CompressAndResizeImage(byte[] imageData, int width, int height, int quality)
{
if (imageData == null || imageData.Length == 0)
{
throw new ArgumentException("图片数据不能为空", nameof(imageData));
}
if (width <= 0 || height <= 0)
{
throw new ArgumentException("宽度和高度必须大于 0");
}
// 确保质量在有效范围内
quality = Math.Clamp(quality, 1, 100);
try
{
using var inputStream = new MemoryStream(imageData);
using var image = Image.Load(inputStream);
using var outputStream = new MemoryStream();
image.Mutate(x => x.Resize(width, height));
var encoder = new JpegEncoder
{
Quality = quality
};
image.Save(outputStream, encoder);
return outputStream.ToArray();
}
catch (Exception ex)
{
_logService?.Error($"压缩并缩放图片失败: {width}x{height}, quality={quality}", ex);
throw;
}
}
#endregion
#region 辅助方法
///
/// 获取当前活动的下载数(用于测试)
///
internal int GetActiveDownloadCount(SemaphoreSlim semaphore, int maxConcurrency)
{
return maxConcurrency - semaphore.CurrentCount;
}
#endregion
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_httpClient.Dispose();
}
_disposed = true;
}
}
#endregion
}
}