using System.IO; using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using Microsoft.Win32; namespace AxCopilot.Views; /// 클립보드 이미지 전용 미리보기 창. 원본 해상도 기준 줌과 저장을 지원합니다. public partial class ClipboardImagePreviewWindow : Window { private const double MinZoom = 0.1; private const double MaxZoom = 8.0; private const double ZoomStep = 0.1; private readonly BitmapSource _image; private readonly string? _sourcePath; private double _zoom = 1.0; private bool _fitToViewport = true; public ClipboardImagePreviewWindow(BitmapSource image, string? sourcePath = null) { _image = image ?? throw new ArgumentNullException(nameof(image)); _sourcePath = sourcePath; InitializeComponent(); PreviewImage.Source = _image; TitleText.Text = string.IsNullOrWhiteSpace(sourcePath) ? "클립보드 이미지 미리보기" : Path.GetFileName(sourcePath); MetaText.Text = $"{_image.PixelWidth:N0} × {_image.PixelHeight:N0}px"; Loaded += (_, _) => { ApplyFitToViewport(); UpdateStatusText(); }; } private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.ClickCount == 2) { WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; return; } try { DragMove(); } catch { } } private void Window_KeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Escape: Close(); e.Handled = true; return; case Key.Add: case Key.OemPlus: IncreaseZoom(); e.Handled = true; return; case Key.Subtract: case Key.OemMinus: DecreaseZoom(); e.Handled = true; return; case Key.D0: case Key.NumPad0: ResetZoom(); e.Handled = true; return; case Key.F: ApplyFitToViewport(); e.Handled = true; return; case Key.C when Keyboard.Modifiers == ModifierKeys.Control: CopyImageToClipboard(); e.Handled = true; return; } } private void ImageScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) return; if (e.Delta > 0) IncreaseZoom(); else if (e.Delta < 0) DecreaseZoom(); e.Handled = true; } private void ImageScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e) { if (_fitToViewport) ApplyFitToViewport(); } private void BtnClose_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => Close(); private void BtnResetZoom_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ResetZoom(); private void BtnZoomIn_Click(object sender, RoutedEventArgs e) => IncreaseZoom(); private void BtnZoomOut_Click(object sender, RoutedEventArgs e) => DecreaseZoom(); private void BtnFit_Click(object sender, RoutedEventArgs e) => ApplyFitToViewport(); private void BtnCopyImage_Click(object sender, RoutedEventArgs e) => CopyImageToClipboard(); private void BtnSavePng_Click(object sender, RoutedEventArgs e) => SaveImage("png"); private void BtnSaveJpeg_Click(object sender, RoutedEventArgs e) => SaveImage("jpg"); private void BtnSaveBmp_Click(object sender, RoutedEventArgs e) => SaveImage("bmp"); private void IncreaseZoom() => SetZoom(_zoom + ZoomStep, false); private void DecreaseZoom() => SetZoom(_zoom - ZoomStep, false); private void ResetZoom() => SetZoom(1.0, false); private void ApplyFitToViewport() { if (ImageScrollViewer.ViewportWidth <= 0 || ImageScrollViewer.ViewportHeight <= 0) return; var availableWidth = Math.Max(1, ImageScrollViewer.ViewportWidth - 32); var availableHeight = Math.Max(1, ImageScrollViewer.ViewportHeight - 32); var scaleX = availableWidth / _image.PixelWidth; var scaleY = availableHeight / _image.PixelHeight; var fitScale = Math.Min(scaleX, scaleY); if (double.IsNaN(fitScale) || double.IsInfinity(fitScale) || fitScale <= 0) fitScale = 1.0; SetZoom(Math.Min(1.0, fitScale), true); } private void SetZoom(double zoom, bool fitToViewport) { _fitToViewport = fitToViewport; _zoom = Math.Max(MinZoom, Math.Min(MaxZoom, zoom)); ImageScaleTransform.ScaleX = _zoom; ImageScaleTransform.ScaleY = _zoom; UpdateStatusText(); } private void UpdateStatusText() { var mode = _fitToViewport ? "창 맞춤" : "수동 줌"; var source = string.IsNullOrWhiteSpace(_sourcePath) ? "원본 경로 없음" : _sourcePath; StatusText.Text = $"{_image.PixelWidth:N0} × {_image.PixelHeight:N0}px · 확대 {Math.Round(_zoom * 100)}% · {mode} · {source}"; } private void CopyImageToClipboard() { try { var app = Application.Current as App; app?.ClipboardHistoryService?.SuppressNextCapture(); Clipboard.SetImage(_image); CustomMessageBox.Show("이미지를 클립보드에 복사했습니다.", "AX Commander", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { Services.LogService.Warn($"클립보드 이미지 복사 실패: {ex.Message}"); CustomMessageBox.Show($"이미지 복사에 실패했습니다.\n{ex.Message}", "AX Commander", MessageBoxButton.OK, MessageBoxImage.Warning); } } private void SaveImage(string format) { try { var dialog = new SaveFileDialog { FileName = BuildDefaultFileName(format), Filter = format switch { "png" => "PNG 이미지|*.png", "jpg" => "JPEG 이미지|*.jpg;*.jpeg", "bmp" => "BMP 이미지|*.bmp", _ => "이미지 파일|*.*", }, AddExtension = true, OverwritePrompt = true, }; if (dialog.ShowDialog(this) != true) return; BitmapEncoder encoder = format switch { "jpg" => new JpegBitmapEncoder { QualityLevel = 95 }, "bmp" => new BmpBitmapEncoder(), _ => new PngBitmapEncoder(), }; encoder.Frames.Add(BitmapFrame.Create(_image)); using var stream = new FileStream(dialog.FileName, FileMode.Create, FileAccess.Write); encoder.Save(stream); CustomMessageBox.Show($"이미지를 저장했습니다.\n{dialog.FileName}", "AX Commander", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { Services.LogService.Warn($"클립보드 이미지 저장 실패: {ex.Message}"); CustomMessageBox.Show($"이미지 저장에 실패했습니다.\n{ex.Message}", "AX Commander", MessageBoxButton.OK, MessageBoxImage.Warning); } } private string BuildDefaultFileName(string format) { var sourceName = !string.IsNullOrWhiteSpace(_sourcePath) ? Path.GetFileNameWithoutExtension(_sourcePath) : $"clipboard-image-{DateTime.Now:yyyyMMdd-HHmmss}"; var extension = format switch { "jpg" => ".jpg", "bmp" => ".bmp", _ => ".png", }; return sourceName + extension; } }