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 } }