diff --git a/src/Everything2Everything.App/App.xaml.cs b/src/Everything2Everything.App/App.xaml.cs index c4d3140..ff19a02 100644 --- a/src/Everything2Everything.App/App.xaml.cs +++ b/src/Everything2Everything.App/App.xaml.cs @@ -107,8 +107,7 @@ public partial class App : Application try { - var options = ConvertOptions.Quick(); - options.VideoPreferGpu = Settings.Get("video.gpu") != "false"; + var options = ConvertOptions.Quick() with { VideoPreferGpu = Settings.Get("video.gpu") != "false" }; var reporter = new Progress(p => progress.Report(p)); var results = await Engine.ConvertManyAsync(files, outputExtension, options, reporter); diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml.cs b/src/Everything2Everything.App/Views/MainWindow.xaml.cs index 52b7a51..d2ed810 100644 --- a/src/Everything2Everything.App/Views/MainWindow.xaml.cs +++ b/src/Everything2Everything.App/Views/MainWindow.xaml.cs @@ -278,38 +278,35 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow private ConvertOptions BuildOptions() { - var opts = new ConvertOptions - { - OnCollision = _conflictRule, - }; - opts.Jpeg.Quality = (int)QualitySlider.Value; - opts.Webp.Quality = (int)QualitySlider.Value; - opts.Avif.Quality = Math.Clamp((int)QualitySlider.Value - 30, 1, 100); + var q = (int)QualitySlider.Value; var custom = OutputPathTextBox.Text?.Trim(); - if (!string.IsNullOrEmpty(custom)) - { - opts.OutputLocation = OutputLocation.Custom; - opts.CustomOutputDirectory = custom; - } - else - { - opts.OutputLocation = OutputLocation.SubfolderBesideSource; - } + var hasCustom = !string.IsNullOrEmpty(custom); // AI 작업 종류 (txt↔txt / md↔md 등 텍스트 변환 시 LlmProvider가 사용) - opts.Ai.Task = (AiTaskCombo?.SelectedIndex ?? 0) switch + var aiTask = (AiTaskCombo?.SelectedIndex ?? 0) switch { 1 => "translate", 2 => "proofread", _ => "summarize", }; var targetLang = AiTargetLangBox?.Text?.Trim(); - opts.Ai.TargetLanguage = string.IsNullOrEmpty(targetLang) ? null : targetLang; - opts.VideoPreferGpu = _settings.Get("video.gpu") != "false"; - - return opts; + return new ConvertOptions + { + OnCollision = _conflictRule, + OutputLocation = hasCustom ? OutputLocation.Custom : OutputLocation.SubfolderBesideSource, + CustomOutputDirectory = hasCustom ? custom : null, + Jpeg = new JpegEncodingOptions { Quality = q }, + Webp = new WebpEncodingOptions { Quality = q }, + Avif = new AvifEncodingOptions { Quality = Math.Clamp(q - 30, 1, 100) }, + Ai = new AiOptions + { + Task = aiTask, + TargetLanguage = string.IsNullOrEmpty(targetLang) ? null : targetLang, + }, + VideoPreferGpu = _settings.Get("video.gpu") != "false", + }; } // ============== Process queue ============== diff --git a/src/Everything2Everything.Core/ConvertOptions.cs b/src/Everything2Everything.Core/ConvertOptions.cs index 1b4dfa4..ef3220d 100644 --- a/src/Everything2Everything.Core/ConvertOptions.cs +++ b/src/Everything2Everything.Core/ConvertOptions.cs @@ -14,154 +14,158 @@ public enum NameCollision Skip } -public sealed class ConvertOptions +/// +/// 변환 옵션. 불변(record + init-only) — 구성된 뒤에는 변경되지 않으므로 배치 병렬 변환에서 안전하게 공유된다. +/// 변경이 필요하면 with 식으로 새 인스턴스를 만든다. (P4: mutable God Object → immutable record) +/// +public sealed record ConvertOptions { - public OutputLocation OutputLocation { get; set; } = OutputLocation.SubfolderBesideSource; + public OutputLocation OutputLocation { get; init; } = OutputLocation.SubfolderBesideSource; - public string SubfolderSuffix { get; set; } = "_converted"; + public string SubfolderSuffix { get; init; } = "_converted"; - public string? CustomOutputDirectory { get; set; } + public string? CustomOutputDirectory { get; init; } - public NameCollision OnCollision { get; set; } = NameCollision.AppendNumber; + public NameCollision OnCollision { get; init; } = NameCollision.AppendNumber; - public int? MaxLongEdgePixels { get; set; } + public int? MaxLongEdgePixels { get; init; } - public bool KeepExifWhenPossible { get; set; } = true; + public bool KeepExifWhenPossible { get; init; } = true; - public bool FlattenTransparency { get; set; } = false; + public bool FlattenTransparency { get; init; } = false; - public string TransparencyBackground { get; set; } = "#FFFFFF"; + public string TransparencyBackground { get; init; } = "#FFFFFF"; - public JpegEncodingOptions Jpeg { get; set; } = new(); + public JpegEncodingOptions Jpeg { get; init; } = new(); - public PngEncodingOptions Png { get; set; } = new(); + public PngEncodingOptions Png { get; init; } = new(); - public WebpEncodingOptions Webp { get; set; } = new(); + public WebpEncodingOptions Webp { get; init; } = new(); - public AvifEncodingOptions Avif { get; set; } = new(); + public AvifEncodingOptions Avif { get; init; } = new(); - public TiffEncodingOptions Tiff { get; set; } = new(); + public TiffEncodingOptions Tiff { get; init; } = new(); - public BmpEncodingOptions Bmp { get; set; } = new(); + public BmpEncodingOptions Bmp { get; init; } = new(); - public GifEncodingOptions Gif { get; set; } = new(); + public GifEncodingOptions Gif { get; init; } = new(); - public PdfRenderOptions PdfRender { get; set; } = new(); + public PdfRenderOptions PdfRender { get; init; } = new(); - public PdfBuildOptions PdfBuild { get; set; } = new(); + public PdfBuildOptions PdfBuild { get; init; } = new(); - public HtmlRenderOptions HtmlRender { get; set; } = new(); + public HtmlRenderOptions HtmlRender { get; init; } = new(); - public OcrOptions Ocr { get; set; } = new(); + public OcrOptions Ocr { get; init; } = new(); // --- 변환 그래프 경로 옵션 (P1) --- /// 멀티홉 경로 자동 합성 허용. false면 직접(1홉) 변환만. - public bool AllowMultiHop { get; set; } = true; + public bool AllowMultiHop { get; init; } = true; /// 멀티홉 최대 홉 수. - public int MaxHops { get; set; } = 3; + public int MaxHops { get; init; } = 3; /// 래스터화 같은 큰 손실 엣지를 회피한다. - public bool AvoidLossy { get; set; } = false; + public bool AvoidLossy { get; init; } = false; /// 영상 인코딩 시 GPU 하드웨어 가속(NVENC)을 우선 시도하고, 실패하면 CPU로 자동 폴백한다. - public bool VideoPreferGpu { get; set; } = true; + public bool VideoPreferGpu { get; init; } = true; - public PdfCompressOptions PdfCompress { get; set; } = new(); + public PdfCompressOptions PdfCompress { get; init; } = new(); - public AiOptions Ai { get; set; } = new(); + public AiOptions Ai { get; init; } = new(); public static ConvertOptions Quick() => new(); } -public sealed class JpegEncodingOptions +public sealed record JpegEncodingOptions { - public int Quality { get; set; } = 92; - public bool Progressive { get; set; } = false; + public int Quality { get; init; } = 92; + public bool Progressive { get; init; } = false; } -public sealed class PngEncodingOptions +public sealed record PngEncodingOptions { - public int Compression { get; set; } = 7; - public bool Interlace { get; set; } = false; + public int Compression { get; init; } = 7; + public bool Interlace { get; init; } = false; } -public sealed class WebpEncodingOptions +public sealed record WebpEncodingOptions { - public int Quality { get; set; } = 90; - public bool Lossless { get; set; } = false; + public int Quality { get; init; } = 90; + public bool Lossless { get; init; } = false; } -public sealed class AvifEncodingOptions +public sealed record AvifEncodingOptions { - public int Quality { get; set; } = 60; - public int Speed { get; set; } = 6; + public int Quality { get; init; } = 60; + public int Speed { get; init; } = 6; } -public sealed class TiffEncodingOptions +public sealed record TiffEncodingOptions { - public string Compression { get; set; } = "lzw"; + public string Compression { get; init; } = "lzw"; } -public sealed class BmpEncodingOptions +public sealed record BmpEncodingOptions { } -public sealed class GifEncodingOptions +public sealed record GifEncodingOptions { } -public sealed class PdfRenderOptions +public sealed record PdfRenderOptions { - public int Dpi { get; set; } = 200; - public bool WithAnnotations { get; set; } = true; - public bool WithFormFill { get; set; } = true; + public int Dpi { get; init; } = 200; + public bool WithAnnotations { get; init; } = true; + public bool WithFormFill { get; init; } = true; } -public sealed class PdfBuildOptions +public sealed record PdfBuildOptions { - public string PageSize { get; set; } = "Auto"; - public int MarginPoints { get; set; } = 24; - public bool FitToPage { get; set; } = true; + public string PageSize { get; init; } = "Auto"; + public int MarginPoints { get; init; } = 24; + public bool FitToPage { get; init; } = true; } -public sealed class HtmlRenderOptions +public sealed record HtmlRenderOptions { - public int ViewportWidth { get; set; } = 1280; - public int? ViewportHeight { get; set; } - public int WaitMilliseconds { get; set; } = 2000; - public bool FullPage { get; set; } = true; + public int ViewportWidth { get; init; } = 1280; + public int? ViewportHeight { get; init; } + public int WaitMilliseconds { get; init; } = 2000; + public bool FullPage { get; init; } = true; } -public sealed class OcrOptions +public sealed record OcrOptions { - public string Language { get; set; } = "ko+en"; - public bool PreserveLayout { get; set; } = true; - public string Backend { get; set; } = "auto"; + public string Language { get; init; } = "ko+en"; + public bool PreserveLayout { get; init; } = true; + public string Backend { get; init; } = "auto"; } -public sealed class PdfCompressOptions +public sealed record PdfCompressOptions { /// Light(구조 최적화·무손실) | Strong(렌더 재인코딩) | Max(Ghostscript). P1은 Light만 구현. - public string Level { get; set; } = "Light"; + public string Level { get; init; } = "Light"; } -public sealed class AiOptions +public sealed record AiOptions { /// auto | openai | anthropic. auto는 설정된 키 중 가용한 것을 선택. - public string Backend { get; set; } = "auto"; + public string Backend { get; init; } = "auto"; /// 모델 ID. null이면 백엔드별 기본값. - public string? Model { get; set; } + public string? Model { get; init; } /// summarize | translate | proofread | custom. - public string Task { get; set; } = "summarize"; + public string Task { get; init; } = "summarize"; /// translate 작업의 대상 언어 (예: "영어", "일본어"). - public string? TargetLanguage { get; set; } + public string? TargetLanguage { get; init; } /// custom 작업의 사용자 지정 지시문. - public string? Instruction { get; set; } + public string? Instruction { get; init; } - public int MaxOutputTokens { get; set; } = 2000; + public int MaxOutputTokens { get; init; } = 2000; } diff --git a/src/Everything2Everything.Core/Converters/OcrProvider.cs b/src/Everything2Everything.Core/Converters/OcrProvider.cs index ae6b259..a9683cf 100644 --- a/src/Everything2Everything.Core/Converters/OcrProvider.cs +++ b/src/Everything2Everything.Core/Converters/OcrProvider.cs @@ -179,8 +179,8 @@ public sealed class OcrProvider : IConverterProvider OutputLocation = OutputLocation.Custom, CustomOutputDirectory = tempDir, OnCollision = NameCollision.Overwrite, + PdfRender = new PdfRenderOptions { Dpi = Math.Max(150, options.PdfRender.Dpi) }, }; - renderOptions.PdfRender.Dpi = Math.Max(150, options.PdfRender.Dpi); var inner = new Progress(p => progress?.Report(p * 0.4)); var result = _pdfProvider.ConvertCore(pdfPath, tempDir, ".png", renderOptions, inner, cancellationToken); diff --git a/src/Everything2Everything.Tests/EngineCharacterizationTests.cs b/src/Everything2Everything.Tests/EngineCharacterizationTests.cs index 43891b5..57457a2 100644 --- a/src/Everything2Everything.Tests/EngineCharacterizationTests.cs +++ b/src/Everything2Everything.Tests/EngineCharacterizationTests.cs @@ -213,8 +213,7 @@ public class EngineCharacterizationTests Assert.Equal(ConvertStatus.Success, first.Status); // 2차: 동일 출력이 존재 + OnCollision=Skip → 마지막 홉(png→jpg)이 Skip을 반환, 엔진은 Skip으로 존중 - var skipOpts = CustomOut(dir); - skipOpts.OnCollision = NameCollision.Skip; + var skipOpts = CustomOut(dir) with { OnCollision = NameCollision.Skip }; var second = await engine.ConvertOneAsync(svg, ".jpg", skipOpts); Assert.Equal(ConvertStatus.Skipped, second.Status); } @@ -230,8 +229,7 @@ public class EngineCharacterizationTests var first = await engine.ConvertOneAsync(png, ".jpg", CustomOut(dir)); Assert.Equal(ConvertStatus.Success, first.Status); - var skipOpts = CustomOut(dir); - skipOpts.OnCollision = NameCollision.Skip; + var skipOpts = CustomOut(dir) with { OnCollision = NameCollision.Skip }; var second = await engine.ConvertOneAsync(png, ".jpg", skipOpts); Assert.Equal(ConvertStatus.Skipped, second.Status); } diff --git a/src/Everything2Everything.Tests/ImageOptimProviderTests.cs b/src/Everything2Everything.Tests/ImageOptimProviderTests.cs index 793a92a..115c527 100644 --- a/src/Everything2Everything.Tests/ImageOptimProviderTests.cs +++ b/src/Everything2Everything.Tests/ImageOptimProviderTests.cs @@ -39,8 +39,7 @@ public class ImageOptimProviderTests using (var img = new MagickImage(MagickColors.SteelBlue, 256, 256)) img.Write(jpg); var p = new ImageOptimProvider(); - var options = new ConvertOptions(); - options.Jpeg.Quality = 30; + var options = new ConvertOptions { Jpeg = new JpegEncodingOptions { Quality = 30 } }; var r = await p.ConvertAsync(jpg, dir, ".jpg", options, null, CancellationToken.None); Assert.Equal(ConvertStatus.Success, r.Status); Assert.True(File.Exists(r.OutputPaths[0]));