using Everything2Everything.Core.Providers; using ImageMagick; namespace Everything2Everything.Core.Converters; /// /// 동일 포맷 이미지를 재인코딩해 용량을 최적화한다(PdfToolProvider의 이미지 버전). /// 명시적 self-edge(jpg→jpg 등)로 그래프에 등록되며, 출력이 입력보다 커지면 원본을 보존한다. /// public sealed class ImageOptimProvider : IConverterProvider { public ProviderCapability Capability { get; } = new( Id: "image-optim", DisplayName: "이미지 최적화 (재압축)", SupportedConversions: new[] { new ConversionPair(".jpg", ".jpg", LossClass.Recode), new ConversionPair(".jpeg", ".jpeg", LossClass.Recode), new ConversionPair(".png", ".png", LossClass.Container), new ConversionPair(".webp", ".webp", LossClass.Recode), new ConversionPair(".tif", ".tif", LossClass.Container), new ConversionPair(".tiff", ".tiff", LossClass.Container), new ConversionPair(".bmp", ".bmp", LossClass.Container), }, Status: ProviderStatus.Available, Summary: "JPEG/PNG/WebP/TIFF/BMP를 같은 포맷으로 재압축해 용량을 줄입니다 (품질 옵션 적용).", ExternalDependencies: Array.Empty(), RoadmapNote: null); public Task CheckAvailabilityAsync(CancellationToken cancellationToken = default) => Task.FromResult(ProviderAvailability.Ready); public Task ConvertAsync( string sourcePath, string outputDirectory, string outputExtension, ConvertOptions options, IProgress? progress, CancellationToken cancellationToken) => Task.Run(() => Optimize(sourcePath, outputDirectory, outputExtension, options, progress, cancellationToken), cancellationToken); private static ConvertResult Optimize( string sourcePath, string outputDirectory, string outputExtension, ConvertOptions options, IProgress? progress, CancellationToken ct) { ct.ThrowIfCancellationRequested(); var outExt = ConversionPair.Normalize(outputExtension); var baseName = Path.GetFileNameWithoutExtension(sourcePath); var outPath = OutputPathHelper.ResolveOutputPath(outputDirectory, baseName, "_optimized", outExt, options.OnCollision); if (OutputPathHelper.ShouldSkip(outPath, options.OnCollision)) return ConvertResult.Skip(sourcePath, "기존 파일이 있어 건너뜁니다."); progress?.Report(0.1); var tmp = outPath + ".tmp"; try { using (var image = new MagickImage(sourcePath)) { try { image.AutoOrient(); } catch { /* orient 실패 무시 */ } if (options.MaxLongEdgePixels is int maxLong && maxLong > 0 && (image.Width > (uint)maxLong || image.Height > (uint)maxLong)) { image.Resize(new MagickGeometry((uint)maxLong, (uint)maxLong) { IgnoreAspectRatio = false }); } ApplyEncoding(image, outExt, options); ct.ThrowIfCancellationRequested(); image.Write(tmp); } progress?.Report(0.9); // 재인코딩이 더 커지면 원본을 사용 var before = new FileInfo(sourcePath).Length; var after = new FileInfo(tmp).Length; if (after >= before) { File.Copy(sourcePath, outPath, overwrite: true); File.Delete(tmp); } else { if (File.Exists(outPath)) File.Delete(outPath); File.Move(tmp, outPath); } } catch (Exception) { try { if (File.Exists(tmp)) File.Delete(tmp); } catch { /* 정리 실패 무시 */ } throw; } progress?.Report(1.0); return ConvertResult.Ok(sourcePath, new[] { outPath }); } private static void ApplyEncoding(IMagickImage image, string outExt, ConvertOptions options) { switch (outExt) { case ".jpg": case ".jpeg": image.Quality = (uint)Math.Clamp(options.Jpeg.Quality, 1, 100); image.Format = MagickFormat.Jpeg; break; case ".png": image.Settings.SetDefine(MagickFormat.Png, "compression-level", Math.Clamp(options.Png.Compression, 0, 9).ToString()); image.Format = MagickFormat.Png; break; case ".webp": image.Quality = (uint)Math.Clamp(options.Webp.Quality, 1, 100); if (options.Webp.Lossless) image.Settings.SetDefine(MagickFormat.WebP, "lossless", "true"); image.Format = MagickFormat.WebP; break; case ".tif": case ".tiff": if (!string.IsNullOrWhiteSpace(options.Tiff.Compression)) image.Settings.SetDefine(MagickFormat.Tiff, "compression", options.Tiff.Compression); image.Format = MagickFormat.Tiff; break; case ".bmp": image.Format = MagickFormat.Bmp; break; } } }