using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using Paca.App.Core; namespace Paca.App.Services; /// /// PACA 데스크톱 애플리케이션 화면 디버깅 캡처 서비스. /// Win32 GDI / PrintWindow / RenderTargetBitmap 하이브리드 파이프라인으로 /// WPF 및 WebView2(HWND) 자식 컨트롤이 포함된 실제 앱 윈도우 전체를 스크린샷으로 캡처하여 저장한다. /// public static class AppDebugScreenshotService { [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left; public int Top; public int Right; public int Bottom; } [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBkgnd, uint nFlags); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool BringWindowToTop(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); private const int SW_RESTORE = 9; private const uint PW_RENDERFULLCONTENT = 0x00000002; private static DispatcherTimer? _triggerTimer; public static string GetDefaultScreenshotPath() => Path.Combine(AppPaths.DataDir, "debug_screenshot.png"); public static string ResolveTriggerPath(string? input) { if (string.IsNullOrWhiteSpace(input)) return GetDefaultScreenshotPath(); return input.Trim(); } /// /// 대상 Window의 현재 화면을 PNG 파일로 캡처하여 저장한다. /// public static bool CaptureWindow(Window window, string outputPath) { if (window == null) return false; try { var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir)) { Directory.CreateDirectory(dir); } var helper = new WindowInteropHelper(window); var hWnd = helper.Handle; if (hWnd != IntPtr.Zero && GetWindowRect(hWnd, out var rect)) { try { ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); BringWindowToTop(hWnd); System.Threading.Thread.Sleep(200); GetWindowRect(hWnd, out rect); } catch { } int width = rect.Right - rect.Left; int height = rect.Bottom - rect.Top; if (width > 0 && height > 0) { try { using var bmp = new System.Drawing.Bitmap(width, height); using (var g = System.Drawing.Graphics.FromImage(bmp)) { var hdc = g.GetHdc(); try { PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT); } finally { g.ReleaseHdc(hdc); } // WebView2 자식 컨트롤 실시간 렌더링 화면 비트맵 오버레이 OverlayAdBanners(window, g, width, height); } bmp.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png); if (File.Exists(outputPath) && new FileInfo(outputPath).Length > 0) { return true; } } catch { // GDI 캡처 실패 시 하단 WPF RenderTargetBitmap 폴백 진행 } } } // 폴백: 순수 WPF 비주얼 트리 렌더링 int rtbWidth = (int)Math.Max(1, window.ActualWidth > 0 ? window.ActualWidth : window.Width); int rtbHeight = (int)Math.Max(1, window.ActualHeight > 0 ? window.ActualHeight : window.Height); if (rtbWidth <= 1) rtbWidth = 1024; if (rtbHeight <= 1) rtbHeight = 768; var rtb = new RenderTargetBitmap(rtbWidth, rtbHeight, 96, 96, PixelFormats.Pbgra32); rtb.Render(window); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(rtb)); using (var fs = File.Create(outputPath)) { encoder.Save(fs); } return File.Exists(outputPath) && new FileInfo(outputPath).Length > 0; } catch { return false; } } /// /// 외부 신호 파일(%AppData%/VideoDownloader/screenshot_trigger.req)을 감지하여 /// 백그라운드에서 실시간 스크린샷을 찍도록 타이머를 구동한다. /// public static void StartFileTriggerWatcher(Window window, Action? onCaptured = null) { if (_triggerTimer != null) return; var triggerFile = Path.Combine(AppPaths.DataDir, "screenshot_trigger.req"); _triggerTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _triggerTimer.Tick += (_, _) => { try { if (File.Exists(triggerFile)) { string target = ""; try { target = File.ReadAllText(triggerFile); File.Delete(triggerFile); } catch { // 읽기 경합 방어 return; } var savePath = ResolveTriggerPath(target); bool ok = CaptureWindow(window, savePath); if (ok) { onCaptured?.Invoke(savePath); } } } catch { // 백그라운드 폴러 방어 } }; _triggerTimer.Start(); } /// /// 앱 실행 후 指定된 지연 시간(기본 3초) 뒤에 자동으로 스크린샷을 찍어 저장한다. /// public static void ScheduleAutoCapture(Window window, int delayMs = 3000, string? targetPath = null) { Task.Delay(delayMs).ContinueWith(_ => { window.Dispatcher.Invoke(() => { var dest = ResolveTriggerPath(targetPath); CaptureWindow(window, dest); }); }); } private static void OverlayAdBanners(Window window, System.Drawing.Graphics g, int windowWidth, int windowHeight) { try { window.Dispatcher.Invoke(() => { var banners = FindVisualChildren(window); foreach (var banner in banners) { if (banner.Visibility != Visibility.Visible || banner.ActualWidth <= 0 || banner.ActualHeight <= 0) continue; var previewPath = Path.Combine(AppPaths.DataDir, "ad_previews", $"ad_{banner.Placement}.png"); if (!File.Exists(previewPath)) continue; try { var transform = banner.TransformToAncestor(window); var point = transform.Transform(new Point(0, 0)); using var fs = new FileStream(previewPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var adImg = System.Drawing.Image.FromStream(fs); int x = (int)point.X; int y = (int)point.Y; int w = (int)banner.ActualWidth; int h = (int)banner.ActualHeight; if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.BottomAppBanner) { x += (w - 728) / 2; y += (h - 90) / 2; w = 728; h = 90; } else if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.DownloadViewBanner) { x += (w - 300) / 2; y += (h - 250) / 2; w = 300; h = 250; } g.DrawImage(adImg, x, y, w, h); } catch (Exception ex) { try { var p = Path.Combine(AppPaths.DataDir, "ad_debug.log"); File.AppendAllText(p, $"[{DateTime.Now:HH:mm:ss.fff}] Overlay exception for {banner.Placement}: {ex.Message}\n"); } catch { } } } }); } catch { } } private static IEnumerable FindVisualChildren(DependencyObject depObj) where T : DependencyObject { if (depObj == null) yield break; int count = VisualTreeHelper.GetChildrenCount(depObj); for (int i = 0; i < count; i++) { var child = VisualTreeHelper.GetChild(depObj, i); if (child is T match) { yield return match; } foreach (var childOfChild in FindVisualChildren(child)) { yield return childOfChild; } } } }