422 lines
13 KiB
C#
422 lines
13 KiB
C#
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
|
using SixLabors.ImageSharp.Processing;
|
|
using WorkCameraExport.Services.Interfaces;
|
|
using Image = SixLabors.ImageSharp.Image;
|
|
|
|
namespace WorkCameraExport.Services
|
|
{
|
|
/// <summary>
|
|
/// 图片服务实现 - 负责图片下载和处理
|
|
/// </summary>
|
|
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)");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 用于测试的构造函数,允许注入 HttpClient
|
|
/// </summary>
|
|
internal ImageService(HttpClient httpClient, ILogService? logService = null)
|
|
{
|
|
_httpClient = httpClient;
|
|
_logService = logService;
|
|
}
|
|
|
|
#region 下载功能
|
|
|
|
/// <summary>
|
|
/// 下载单张图片
|
|
/// </summary>
|
|
public async Task<byte[]?> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 批量并发下载图片
|
|
/// 使用 SemaphoreSlim 控制并发数
|
|
/// </summary>
|
|
public async Task<Dictionary<string, string?>> DownloadImagesAsync(
|
|
List<string> urls,
|
|
string outputDir,
|
|
int concurrency,
|
|
IProgress<int>? progress = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (urls == null || urls.Count == 0)
|
|
{
|
|
return new Dictionary<string, string?>();
|
|
}
|
|
|
|
// 确保并发数在有效范围内
|
|
concurrency = Math.Clamp(concurrency, 1, 10);
|
|
|
|
// 确保输出目录存在
|
|
if (!Directory.Exists(outputDir))
|
|
{
|
|
Directory.CreateDirectory(outputDir);
|
|
}
|
|
|
|
var results = new Dictionary<string, string?>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 下载图片到文件
|
|
/// </summary>
|
|
private async Task<string?> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 URL 提取文件名
|
|
/// </summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 确保文件路径唯一
|
|
/// </summary>
|
|
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 图片处理功能
|
|
|
|
/// <summary>
|
|
/// 压缩图片
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 缩放图片
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 压缩并缩放图片
|
|
/// </summary>
|
|
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 辅助方法
|
|
|
|
/// <summary>
|
|
/// 获取当前活动的下载数(用于测试)
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|