1
0
Fork 0

feat: Everything2Everything 양방향 변환 매트릭스 피보팅

EverythingToJpeg(N→JPEG 단방향)에서 Everything2Everything(N×M 매트릭스)으로
앱을 피보팅. 7개 Provider, 200+ 변환 쌍 지원.

리네이밍
- 디렉토리/솔루션/csproj/namespace/AssemblyName/MSIX Identity 일괄 치환
- 임시파일 prefix e2j_ → e2e_, 로고 라벨 E2J → E2E

아키텍처
- ConversionPair(input, output) 신규 + ProviderCapability.SupportedConversions
- IConverterProvider.ConvertAsync에 outputExtension 파라미터 추가
- ProviderRegistry를 Dictionary<(input, output), Provider> 매트릭스로 재작성
  + OutputsForInput / OutputsForFile 쿼리 API
- ConversionEngine: 출력 ext 라우팅, 동일 입출력 자동 skip,
  서브폴더 suffix를 출력 ext에서 자동 도출
- ConvertOptions를 형식별 sub-record로 분리
  (Jpeg/Png/Webp/Avif/Tiff/Pdf*/Html*/Ocr)

Provider 매트릭스
- MagickProvider: PNG/JPEG/WebP/AVIF/BMP/TIFF/GIF/PDF 양방향 + RAW/PSD 디코딩
  (단일이미지→1페이지 PDF, GIF/TIFF→다페이지 PDF)
- HeicProvider: HEIC/HEIF → 이미지 7종
- PdfProvider: PDF → 이미지 6종 (PNG임베드 후 ImageMagick 인코딩)
- HtmlProvider: HTML/HTM → 이미지 + PDF (CDP printToPDF)
- DocxProvider/HwpxProvider: PDF + 이미지 7종 (LibreOffice 활용)
- OcrProvider 신규: 이미지/PDF → TXT/DOCX
  (Windows.Media.Ocr + DocumentFormat.OpenXml, PDF는 페이지별 OCR 후 결합)

UI
- 사이드바 TARGET FORMAT을 동적 ComboBox로 (큐 파일 매트릭스의 교집합만 표시)
- 출력 형식별 색상 배지 + Quality 패널 라벨 동적
- OUTPUT DESTINATION hint도 출력 ext 기반 동적

CLI
- 신규 'to <ext> <files...>' verb (예: to png photo.heic doc.docx)
- help 텍스트 양방향 컨셉으로 갱신

셸 통합
- ContextMenuRegistrar 카스케이드로 재설계
  (ExtendedSubCommandsKey + Everything2Everything.SubMenu.<ext>)
- 입력별 매트릭스에 따라 인기 출력 10종(JPEG/PNG/WebP/PDF/TXT/DOCX/AVIF/GIF/TIFF/BMP)
  동적 노출, 마지막에 '변환…' 옵션
- 메뉴 라벨 'JPEG로 변환' → 'Everything2Everything으로 변환'

기타
- Core.csproj TFM net9.0-windows → net9.0-windows10.0.19041.0 (WinRT API용)
- DocumentFormat.OpenXml 3.1.0 NuGet 추가
- README 양방향 매트릭스 기준 전면 개편
- MSIX appxmanifest Description / IExplorerCommand DLL 라벨 갱신

빌드: 0 errors, 0 warnings
This commit is contained in:
Yun Chan 2026-05-07 13:40:08 +09:00
parent 96861e627d
commit 9b7e5f0d0c
63 changed files with 1788 additions and 733 deletions

View file

@ -0,0 +1,139 @@
using Everything2Everything.App.Shell;
using Everything2Everything.Core;
namespace Everything2Everything.App.Cli;
internal static class CliRouter
{
public enum Mode
{
ShowMain,
Quick,
Dialog,
Register,
Unregister,
Diagnose,
Help,
}
public sealed record ParsedArgs(Mode Mode, IReadOnlyList<string> Files, string? OutputExtension = null);
public static ParsedArgs Parse(string[] args)
{
if (args is null || args.Length == 0)
return new ParsedArgs(Mode.ShowMain, Array.Empty<string>());
var verb = args[0].Trim().ToLowerInvariant();
var rest = args.Skip(1).Where(a => !string.IsNullOrWhiteSpace(a)).ToList();
if (verb == "to")
{
if (rest.Count < 2)
return new ParsedArgs(Mode.Help, Array.Empty<string>());
var outputExt = NormalizeExt(rest[0]);
var files = ExpandFiles(rest.Skip(1));
return new ParsedArgs(Mode.Quick, files, outputExt);
}
return verb switch
{
"quick" => new ParsedArgs(Mode.Quick, ExpandFiles(rest), ".jpg"),
"dialog" => new ParsedArgs(Mode.Dialog, ExpandFiles(rest)),
"register" => new ParsedArgs(Mode.Register, Array.Empty<string>()),
"unregister" => new ParsedArgs(Mode.Unregister, Array.Empty<string>()),
"diagnose" or "doctor" => new ParsedArgs(Mode.Diagnose, Array.Empty<string>()),
"help" or "--help" or "-h" or "/?" => new ParsedArgs(Mode.Help, Array.Empty<string>()),
_ when File.Exists(args[0]) => new ParsedArgs(Mode.Dialog, ExpandFiles(args)),
_ => new ParsedArgs(Mode.ShowMain, Array.Empty<string>()),
};
}
private static string NormalizeExt(string ext)
{
var trimmed = ext.Trim().ToLowerInvariant();
return trimmed.StartsWith('.') ? trimmed : "." + trimmed;
}
private static IReadOnlyList<string> ExpandFiles(IEnumerable<string> raw)
{
var list = new List<string>();
foreach (var arg in raw)
{
if (string.IsNullOrWhiteSpace(arg)) continue;
try
{
if (File.Exists(arg)) { list.Add(Path.GetFullPath(arg)); continue; }
if (Directory.Exists(arg))
{
foreach (var f in Directory.EnumerateFiles(arg, "*", SearchOption.TopDirectoryOnly))
list.Add(Path.GetFullPath(f));
}
}
catch { }
}
return list;
}
public static string HelpText()
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var inputs = string.Join(", ",
engine.Providers.Implemented.SelectMany(p => p.Capability.InputExtensions).Distinct().OrderBy(e => e));
var outputs = string.Join(", ",
engine.Providers.Implemented.SelectMany(p => p.Capability.OutputExtensions).Distinct().OrderBy(e => e));
var coming = string.Join(", ",
engine.Providers.ComingSoon.SelectMany(p => p.Capability.InputExtensions).Distinct().OrderBy(e => e));
return $"""
Everything2Everything ( )
:
Everything2Everything.exe to <ext> <...> <ext> (: to png a.jpg b.heic)
Everything2Everything.exe quick <...> ( .jpg)
Everything2Everything.exe dialog <...>
Everything2Everything.exe register ( )
Everything2Everything.exe unregister
Everything2Everything.exe diagnose ·
Everything2Everything.exe
: {inputs}
: {outputs}
: {coming}
""";
}
public static int RunRegister(bool register)
{
var logPath = Path.Combine(Path.GetTempPath(), "Everything2Everything_register.log");
var log = new System.Text.StringBuilder();
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] RunRegister start, register={register}");
try
{
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Creating engine...");
var engine = Everything2EverythingBootstrap.CreateDefault();
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Engine OK. Implemented providers: {string.Join(",", engine.Providers.Implemented.Select(p => p.Capability.Id))}");
if (register)
{
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Calling Register...");
ContextMenuRegistrar.Register(engine);
}
else
{
ContextMenuRegistrar.Unregister(engine);
}
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] OK");
File.WriteAllText(logPath, log.ToString());
Console.Out.WriteLine($"등록 완료. 로그: {logPath}");
return 0;
}
catch (Exception ex)
{
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] EXCEPTION {ex.GetType().Name}: {ex.Message}");
log.AppendLine(ex.ToString());
try { File.WriteAllText(logPath, log.ToString()); } catch { }
Console.Error.WriteLine(ex.Message);
return 1;
}
}
}

View file

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
namespace Everything2Everything.App.Cli;
internal static class ConsoleHelper
{
private static bool _attached;
public static void WriteLine(string text)
{
EnsureAttached();
Console.Out.WriteLine(text);
Console.Out.Flush();
}
private static void EnsureAttached()
{
if (_attached) return;
_attached = AttachConsole(-1);
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);
}