Phase 3: HTML(WebView2) + HWP/HWPX(LibreOffice) 실구현
HTML / HTM - HtmlProvider: STA 스레드 + WPF Dispatcher에서 WebView2 헤드리스 컨트롤러(HWND_MESSAGE) 생성 - CDP Page.captureScreenshot(captureBeyondViewport=true)으로 풀페이지 PNG 획득 - Magick.NET으로 JPEG 인코드, 알파/리사이즈/품질 옵션 일관 적용 - ConvertOptions: HtmlViewportWidth/Height, HtmlWaitMilliseconds, HtmlFullPage HWP / HWPX - HwpxProvider: LibreOffice headless --convert-to pdf + H2Orestart 확장 자동 감지 - DocxProvider 패턴 재사용 (소스→PDF→PdfProvider 위임) - ExternalToolDetector.IsH2OrestartInstalled — uno_packages/extensions 검색 기타 - Core csproj: UseWPF=true (WebView2가 WPF 의존), Microsoft.Web.WebView2 1.0.3912.50 추가 - AppX 매니페스트에 .html/.htm/.hwp/.hwpx ItemType 추가 - 파일 다이얼로그 필터 확장 - 두 ComingSoon 상태였던 Provider가 이제 Available/RequiresExternal로 노출됨 검증 - 솔루션 Release 빌드 통과 - MSIX 38개 페이로드 패키징 성공 (WebView2 SDK 포함) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2c8610ff6
commit
5750aed16f
9 changed files with 366 additions and 26 deletions
|
|
@ -73,7 +73,7 @@ public partial class ConvertWindow : FluentWindow
|
|||
{
|
||||
Multiselect = true,
|
||||
Title = "추가할 파일 선택",
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc|모든 파일|*.*",
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc;*.html;*.htm;*.hwp;*.hwpx|모든 파일|*.*",
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true) AddFiles(dlg.FileNames);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ public partial class MainWindow : FluentWindow, INotifyPropertyChanged
|
|||
{
|
||||
Title = "변환할 파일 선택",
|
||||
Multiselect = true,
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc|모든 파일|*.*",
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc;*.html;*.htm;*.hwp;*.hwpx|모든 파일|*.*",
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,5 +36,13 @@ public sealed class ConvertOptions
|
|||
|
||||
public string TransparencyBackground { get; set; } = "#FFFFFF";
|
||||
|
||||
public int HtmlViewportWidth { get; set; } = 1280;
|
||||
|
||||
public int? HtmlViewportHeight { get; set; }
|
||||
|
||||
public int HtmlWaitMilliseconds { get; set; } = 2000;
|
||||
|
||||
public bool HtmlFullPage { get; set; } = true;
|
||||
|
||||
public static ConvertOptions Quick() => new();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,4 +48,38 @@ internal static class ExternalToolDetector
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsH2OrestartInstalled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var roots = new[]
|
||||
{
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
};
|
||||
foreach (var root in roots)
|
||||
{
|
||||
if (string.IsNullOrEmpty(root)) continue;
|
||||
var loDir = Path.Combine(root, "LibreOffice", "4", "user", "uno_packages", "cache", "uno_packages");
|
||||
if (Directory.Exists(loDir))
|
||||
{
|
||||
foreach (var dir in Directory.EnumerateDirectories(loDir, "*H2Orestart*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (Directory.Exists(dir)) return true;
|
||||
}
|
||||
}
|
||||
var extDir = Path.Combine(root, "LibreOffice", "4", "user", "extensions", "bundled");
|
||||
if (Directory.Exists(extDir))
|
||||
{
|
||||
foreach (var dir in Directory.EnumerateDirectories(extDir, "*H2O*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (Directory.Exists(dir)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
using ImageMagick;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace EverythingToJpeg.Core.Converters;
|
||||
|
||||
|
|
@ -8,8 +14,8 @@ public sealed class HtmlProvider : IConverterProvider
|
|||
Id: "html",
|
||||
DisplayName: "HTML / 웹 페이지",
|
||||
Extensions: new[] { ".html", ".htm" },
|
||||
Status: ProviderStatus.ComingSoon,
|
||||
Summary: "HTML/HTM 파일을 WebView2로 헤드리스 렌더링하여 JPEG로 캡처합니다.",
|
||||
Status: ProviderStatus.Available,
|
||||
Summary: "HTML/HTM 파일을 WebView2로 헤드리스 렌더링하여 풀페이지 JPEG로 캡처합니다.",
|
||||
ExternalDependencies: new[]
|
||||
{
|
||||
new ExternalDependency(
|
||||
|
|
@ -18,13 +24,176 @@ public sealed class HtmlProvider : IConverterProvider
|
|||
DownloadUrl: "https://developer.microsoft.com/microsoft-edge/webview2/",
|
||||
IsRequired: true),
|
||||
},
|
||||
RoadmapNote: "Phase 2 — WebView2 헤드리스 캡처 + 사용자 정의 viewport.");
|
||||
RoadmapNote: null);
|
||||
|
||||
public Task<ProviderAvailability> CheckAvailabilityAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(ProviderAvailability.NotReady("아직 구현되지 않았습니다. 곧 지원 예정입니다."));
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
if (string.IsNullOrEmpty(version))
|
||||
return Task.FromResult(ProviderAvailability.NotReady(
|
||||
"WebView2 Runtime이 설치되어 있지 않습니다.",
|
||||
Capability.ExternalDependencies));
|
||||
return Task.FromResult(ProviderAvailability.Ready);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(ProviderAvailability.NotReady(
|
||||
"WebView2 감지 실패: " + ex.Message,
|
||||
Capability.ExternalDependencies));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ConvertResult> ConvertAsync(
|
||||
string sourcePath, string outputDirectory, ConvertOptions options,
|
||||
IProgress<double>? progress, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(ConvertResult.Skip(sourcePath, "HTML 변환은 곧 지원 예정입니다."));
|
||||
public async Task<ConvertResult> ConvertAsync(
|
||||
string sourcePath,
|
||||
string outputDirectory,
|
||||
ConvertOptions options,
|
||||
IProgress<double>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var baseName = Path.GetFileNameWithoutExtension(sourcePath);
|
||||
var path = OutputPathHelper.ResolveOutputPath(outputDirectory, baseName, null, options.OnCollision);
|
||||
if (OutputPathHelper.ShouldSkip(path, options.OnCollision))
|
||||
return ConvertResult.Skip(sourcePath, "기존 파일이 있어 건너뜁니다.");
|
||||
|
||||
var pngBytes = await CapturePngAsync(sourcePath, options, progress, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
progress?.Report(0.85);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
using var image = new MagickImage(pngBytes);
|
||||
if (options.FlattenTransparency && image.HasAlpha)
|
||||
{
|
||||
image.BackgroundColor = new MagickColor(options.TransparencyBackground);
|
||||
image.Alpha(AlphaOption.Remove);
|
||||
image.Alpha(AlphaOption.Off);
|
||||
}
|
||||
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 });
|
||||
}
|
||||
image.Quality = (uint)Math.Clamp(options.Quality, 1, 100);
|
||||
image.Format = MagickFormat.Jpeg;
|
||||
image.Write(path);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress?.Report(1.0);
|
||||
return ConvertResult.Ok(sourcePath, new[] { path });
|
||||
}
|
||||
|
||||
private static Task<byte[]> CapturePngAsync(
|
||||
string sourcePath,
|
||||
ConvertOptions options,
|
||||
IProgress<double>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var dispatcher = Dispatcher.CurrentDispatcher;
|
||||
_ = RunCaptureOnDispatcher(dispatcher, sourcePath, options, progress, cancellationToken, tcs);
|
||||
Dispatcher.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.IsBackground = true;
|
||||
thread.Name = "EverythingToJpeg.HtmlCapture";
|
||||
thread.Start();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static async Task RunCaptureOnDispatcher(
|
||||
Dispatcher dispatcher,
|
||||
string sourcePath,
|
||||
ConvertOptions options,
|
||||
IProgress<double>? progress,
|
||||
CancellationToken cancellationToken,
|
||||
TaskCompletionSource<byte[]> tcs)
|
||||
{
|
||||
CoreWebView2Controller? controller = null;
|
||||
try
|
||||
{
|
||||
var userDataFolder = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"EverythingToJpeg", "WebView2");
|
||||
Directory.CreateDirectory(userDataFolder);
|
||||
|
||||
var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder).ConfigureAwait(true);
|
||||
progress?.Report(0.15);
|
||||
|
||||
// HWND_MESSAGE = (IntPtr)(-3) → headless message-only parent
|
||||
controller = await env.CreateCoreWebView2ControllerAsync(new IntPtr(-3)).ConfigureAwait(true);
|
||||
|
||||
int width = options.HtmlViewportWidth > 0 ? options.HtmlViewportWidth : 1280;
|
||||
int height = options.HtmlViewportHeight ?? 720;
|
||||
controller.Bounds = new System.Drawing.Rectangle(0, 0, width, height);
|
||||
controller.IsVisible = false;
|
||||
|
||||
var web = controller.CoreWebView2;
|
||||
progress?.Report(0.3);
|
||||
|
||||
var navTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
EventHandler<CoreWebView2NavigationCompletedEventArgs>? navHandler = null;
|
||||
navHandler = (_, e) =>
|
||||
{
|
||||
web.NavigationCompleted -= navHandler!;
|
||||
if (e.IsSuccess) navTcs.TrySetResult(true);
|
||||
else navTcs.TrySetException(new InvalidOperationException(
|
||||
$"내비게이션 실패: {e.WebErrorStatus}"));
|
||||
};
|
||||
web.NavigationCompleted += navHandler;
|
||||
|
||||
var fileUri = new Uri(sourcePath).AbsoluteUri;
|
||||
web.Navigate(fileUri);
|
||||
|
||||
using (cancellationToken.Register(() => navTcs.TrySetCanceled()))
|
||||
{
|
||||
await navTcs.Task.ConfigureAwait(true);
|
||||
}
|
||||
progress?.Report(0.5);
|
||||
|
||||
if (options.HtmlWaitMilliseconds > 0)
|
||||
await Task.Delay(options.HtmlWaitMilliseconds, cancellationToken).ConfigureAwait(true);
|
||||
|
||||
progress?.Report(0.65);
|
||||
|
||||
// Use CDP for full-page screenshot beyond viewport
|
||||
var captureParams = options.HtmlFullPage
|
||||
? "{\"captureBeyondViewport\":true,\"format\":\"png\"}"
|
||||
: "{\"format\":\"png\"}";
|
||||
|
||||
var resultJson = await web
|
||||
.CallDevToolsProtocolMethodAsync("Page.captureScreenshot", captureParams)
|
||||
.ConfigureAwait(true);
|
||||
progress?.Report(0.8);
|
||||
|
||||
using var doc = JsonDocument.Parse(resultJson);
|
||||
var b64 = doc.RootElement.GetProperty("data").GetString()
|
||||
?? throw new InvalidOperationException("CDP captureScreenshot이 빈 결과를 반환했습니다.");
|
||||
var pngBytes = Convert.FromBase64String(b64);
|
||||
|
||||
tcs.TrySetResult(pngBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { controller?.Close(); } catch { }
|
||||
dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,134 @@
|
|||
using System.Diagnostics;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
|
||||
namespace EverythingToJpeg.Core.Converters;
|
||||
|
||||
public sealed class HwpxProvider : IConverterProvider
|
||||
{
|
||||
private readonly PdfProvider _pdfProvider;
|
||||
|
||||
public HwpxProvider() : this(new PdfProvider()) { }
|
||||
|
||||
public HwpxProvider(PdfProvider pdfProvider)
|
||||
{
|
||||
_pdfProvider = pdfProvider;
|
||||
}
|
||||
|
||||
public ProviderCapability Capability { get; } = new(
|
||||
Id: "hwpx",
|
||||
DisplayName: "한글 문서 (HWP / HWPX)",
|
||||
Extensions: new[] { ".hwp", ".hwpx" },
|
||||
Status: ProviderStatus.ComingSoon,
|
||||
Summary: "한글(HWP/HWPX) 문서를 PDF로 변환한 뒤 페이지별 JPEG로 저장합니다.",
|
||||
Status: ProviderStatus.RequiresExternal,
|
||||
Summary: "한글(HWP/HWPX) 문서를 LibreOffice + H2Orestart로 PDF 변환 후 페이지별 JPEG로 저장합니다.",
|
||||
ExternalDependencies: new[]
|
||||
{
|
||||
new ExternalDependency(
|
||||
Name: "LibreOffice + H2Orestart 확장",
|
||||
Description: "한글 파일을 LibreOffice가 읽도록 해 주는 오픈소스 확장입니다.",
|
||||
DownloadUrl: "https://github.com/ebandal/H2Orestart",
|
||||
Name: "LibreOffice",
|
||||
Description: "한글 변환에 필요한 헤드리스 오피스 엔진.",
|
||||
DownloadUrl: "https://www.libreoffice.org/download/",
|
||||
IsRequired: true),
|
||||
new ExternalDependency(
|
||||
Name: "H2Orestart 확장",
|
||||
Description: "LibreOffice가 한글 파일을 읽도록 하는 오픈소스 확장. 다운로드한 oxt 파일을 LibreOffice에서 더블클릭해 설치.",
|
||||
DownloadUrl: "https://github.com/ebandal/H2Orestart/releases",
|
||||
IsRequired: true),
|
||||
},
|
||||
RoadmapNote: "Phase 2 — H2Orestart + soffice headless 파이프라인. 한컴오피스 SDK 연동도 검토.");
|
||||
RoadmapNote: null);
|
||||
|
||||
public Task<ProviderAvailability> CheckAvailabilityAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(ProviderAvailability.NotReady("아직 구현되지 않았습니다. 곧 지원 예정입니다."));
|
||||
{
|
||||
if (!ExternalToolDetector.TryFindLibreOfficeSoffice(out _))
|
||||
return Task.FromResult(ProviderAvailability.NotReady(
|
||||
"LibreOffice가 설치되어 있지 않습니다.",
|
||||
Capability.ExternalDependencies));
|
||||
|
||||
public Task<ConvertResult> ConvertAsync(
|
||||
string sourcePath, string outputDirectory, ConvertOptions options,
|
||||
IProgress<double>? progress, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(ConvertResult.Skip(sourcePath, "HWP/HWPX 변환은 곧 지원 예정입니다."));
|
||||
if (!ExternalToolDetector.IsH2OrestartInstalled())
|
||||
return Task.FromResult(ProviderAvailability.NotReady(
|
||||
"H2Orestart 확장이 설치되어 있지 않습니다. https://github.com/ebandal/H2Orestart/releases 에서 .oxt 다운로드 후 LibreOffice에서 설치하세요.",
|
||||
Capability.ExternalDependencies));
|
||||
|
||||
return Task.FromResult(ProviderAvailability.Ready);
|
||||
}
|
||||
|
||||
public async Task<ConvertResult> ConvertAsync(
|
||||
string sourcePath,
|
||||
string outputDirectory,
|
||||
ConvertOptions options,
|
||||
IProgress<double>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ExternalToolDetector.TryFindLibreOfficeSoffice(out var soffice))
|
||||
return ConvertResult.Fail(sourcePath, "LibreOffice가 필요합니다.");
|
||||
|
||||
var tempPdf = Path.Combine(Path.GetTempPath(),
|
||||
$"e2j_{Guid.NewGuid():N}_{Path.GetFileNameWithoutExtension(sourcePath)}.pdf");
|
||||
|
||||
try
|
||||
{
|
||||
progress?.Report(0.05);
|
||||
|
||||
var converted = await ConvertWithLibreOfficeAsync(soffice, sourcePath, tempPdf, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!converted)
|
||||
return ConvertResult.Fail(sourcePath,
|
||||
"LibreOffice 변환에 실패했습니다. H2Orestart 확장이 정상 설치되어 있는지 확인하세요.");
|
||||
|
||||
progress?.Report(0.55);
|
||||
|
||||
var inner = new Progress<double>(p => progress?.Report(0.55 + p * 0.45));
|
||||
return _pdfProvider.ConvertCore(tempPdf, outputDirectory, options, inner, cancellationToken)
|
||||
with { SourcePath = sourcePath };
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(tempPdf)) File.Delete(tempPdf); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> ConvertWithLibreOfficeAsync(string sofficePath, string sourcePath, string targetPdf, CancellationToken ct)
|
||||
{
|
||||
var outDir = Path.GetDirectoryName(targetPdf)!;
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = sofficePath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
psi.ArgumentList.Add("--headless");
|
||||
psi.ArgumentList.Add("--norestore");
|
||||
psi.ArgumentList.Add("--nofirststartwizard");
|
||||
psi.ArgumentList.Add("--convert-to");
|
||||
psi.ArgumentList.Add("pdf");
|
||||
psi.ArgumentList.Add("--outdir");
|
||||
psi.ArgumentList.Add(outDir);
|
||||
psi.ArgumentList.Add(sourcePath);
|
||||
|
||||
using var proc = Process.Start(psi);
|
||||
if (proc is null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
await proc.WaitForExitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try { proc.Kill(true); } catch { }
|
||||
throw;
|
||||
}
|
||||
|
||||
if (proc.ExitCode != 0) return false;
|
||||
|
||||
var produced = Path.Combine(outDir, Path.GetFileNameWithoutExtension(sourcePath) + ".pdf");
|
||||
if (!File.Exists(produced)) return false;
|
||||
|
||||
if (!string.Equals(produced, targetPdf, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (File.Exists(targetPdf)) File.Delete(targetPdf);
|
||||
File.Move(produced, targetPdf);
|
||||
}
|
||||
return File.Exists(targetPdf);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<UseWindowsForms>false</UseWindowsForms>
|
||||
<UseWPF>false</UseWPF>
|
||||
<UseWPF>true</UseWPF>
|
||||
<NoWarn>$(NoWarn);NU1901;NU1902;NU1903;NU1904</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
@ -15,6 +15,11 @@
|
|||
<PackageReference Include="PDFtoImage" Version="5.2.1" />
|
||||
<PackageReference Include="PhotoSauce.MagicScaler" Version="0.15.0" />
|
||||
<PackageReference Include="PhotoSauce.NativeCodecs.Libheif" Version="1.19.5-preview1" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3912.50" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System.IO" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue