refactor(P4): ConvertOptions God Object → 불변 record (init-only)
mutable sealed class(13 sub-record 가변)을 불변 record로 전환 — 배치 병렬 변환(P6)에서 공유해도 안전한 스레드 안전 전제 확보 + 값 의미론. 시그니처(ConvertAsync(ConvertOptions))는 비침습 유지. - ConvertOptions + 13개 옵션 타입(Jpeg/Png/Webp/Avif/Tiff/Bmp/Gif/PdfRender/PdfBuild/HtmlRender/ Ocr/PdfCompress/Ai) → sealed record, 전 프로퍼티 get;init;. 8개 Provider의 READ는 무영향. - 변이 5곳을 with 식/객체 초기화로 교정(WRITE만 깨지므로 국소적): MainWindow.BuildOptions(한 식으로 불변 구성), App.RunQuickAsync(with), OcrProvider(PdfRender 초기화), 특성화 테스트 2곳, ImageOptimProviderTests. - 카테고리 분해(RoutingOptions/OutputOptions)는 reader 파급이 커서 후속으로 보류(비침습 우선). 70개 테스트 전부 그린(골든마스터 동일성 = 동작 불변), 빌드 0경고/0오류.
This commit is contained in:
parent
12bda2dcbf
commit
379a4a738d
6 changed files with 95 additions and 98 deletions
|
|
@ -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<ConvertProgress>(p => progress.Report(p));
|
||||
var results = await Engine.ConvertManyAsync(files, outputExtension, options, reporter);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ==============
|
||||
|
|
|
|||
|
|
@ -14,154 +14,158 @@ public enum NameCollision
|
|||
Skip
|
||||
}
|
||||
|
||||
public sealed class ConvertOptions
|
||||
/// <summary>
|
||||
/// 변환 옵션. 불변(record + init-only) — 구성된 뒤에는 변경되지 않으므로 배치 병렬 변환에서 안전하게 공유된다.
|
||||
/// 변경이 필요하면 with 식으로 새 인스턴스를 만든다. (P4: mutable God Object → immutable record)
|
||||
/// </summary>
|
||||
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) ---
|
||||
/// <summary>멀티홉 경로 자동 합성 허용. false면 직접(1홉) 변환만.</summary>
|
||||
public bool AllowMultiHop { get; set; } = true;
|
||||
public bool AllowMultiHop { get; init; } = true;
|
||||
|
||||
/// <summary>멀티홉 최대 홉 수.</summary>
|
||||
public int MaxHops { get; set; } = 3;
|
||||
public int MaxHops { get; init; } = 3;
|
||||
|
||||
/// <summary>래스터화 같은 큰 손실 엣지를 회피한다.</summary>
|
||||
public bool AvoidLossy { get; set; } = false;
|
||||
public bool AvoidLossy { get; init; } = false;
|
||||
|
||||
/// <summary>영상 인코딩 시 GPU 하드웨어 가속(NVENC)을 우선 시도하고, 실패하면 CPU로 자동 폴백한다.</summary>
|
||||
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
|
||||
{
|
||||
/// <summary>Light(구조 최적화·무손실) | Strong(렌더 재인코딩) | Max(Ghostscript). P1은 Light만 구현.</summary>
|
||||
public string Level { get; set; } = "Light";
|
||||
public string Level { get; init; } = "Light";
|
||||
}
|
||||
|
||||
public sealed class AiOptions
|
||||
public sealed record AiOptions
|
||||
{
|
||||
/// <summary>auto | openai | anthropic. auto는 설정된 키 중 가용한 것을 선택.</summary>
|
||||
public string Backend { get; set; } = "auto";
|
||||
public string Backend { get; init; } = "auto";
|
||||
|
||||
/// <summary>모델 ID. null이면 백엔드별 기본값.</summary>
|
||||
public string? Model { get; set; }
|
||||
public string? Model { get; init; }
|
||||
|
||||
/// <summary>summarize | translate | proofread | custom.</summary>
|
||||
public string Task { get; set; } = "summarize";
|
||||
public string Task { get; init; } = "summarize";
|
||||
|
||||
/// <summary>translate 작업의 대상 언어 (예: "영어", "일본어").</summary>
|
||||
public string? TargetLanguage { get; set; }
|
||||
public string? TargetLanguage { get; init; }
|
||||
|
||||
/// <summary>custom 작업의 사용자 지정 지시문.</summary>
|
||||
public string? Instruction { get; set; }
|
||||
public string? Instruction { get; init; }
|
||||
|
||||
public int MaxOutputTokens { get; set; } = 2000;
|
||||
public int MaxOutputTokens { get; init; } = 2000;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<double>(p => progress?.Report(p * 0.4));
|
||||
var result = _pdfProvider.ConvertCore(pdfPath, tempDir, ".png", renderOptions, inner, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue