using System.Collections.Concurrent;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
namespace AxCopilot.Services;
///
/// Shell32 SHGetFileInfo를 사용하여 파일/폴더의 Windows 아이콘을 추출·캐시합니다.
/// 캐시 위치: %LOCALAPPDATA%\AxCopilot\IconCache\{확장자}.png
/// GetIconPath()는 캐시 미스 시 null을 반환하고 백그라운드에서 추출을 시작합니다.
/// WarmUp()을 앱 시작 시 호출하면 자주 쓰는 확장자를 미리 준비합니다.
///
internal static class IconCacheService
{
private static readonly string _cacheDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"AxCopilot", "IconCache");
/// key(확장자 또는 "folder") → PNG 캐시 파일 경로 (null = 추출 실패)
private static readonly ConcurrentDictionary _cache =
new(StringComparer.OrdinalIgnoreCase);
private static volatile bool _warmupDone;
// ─── Win32 ──────────────────────────────────────────────────────────────
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] public string szTypeName;
}
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SHGetFileInfo(
string pszPath, uint dwFileAttributes,
ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr hIcon);
private const uint SHGFI_ICON = 0x000000100;
private const uint SHGFI_LARGEICON = 0x000000000;
private const uint SHGFI_USEFILEATTRIBUTES = 0x000000010;
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
// ─── 공개 API ───────────────────────────────────────────────────────────
///
/// 파일 확장자 기반 아이콘 PNG 경로를 반환합니다.
/// 캐시가 없으면 백그라운드에서 추출을 시작하고 null을 반환합니다.
///
public static string? GetIconPath(string filePath, bool isDirectory = false)
{
var key = isDirectory ? "folder" : GetExtKey(filePath);
if (_cache.TryGetValue(key, out var cached)) return cached;
_ = ExtractAsync(filePath, key, isDirectory);
return null;
}
///
/// 앱 시작 시 자주 쓰는 파일 형식의 아이콘을 미리 추출합니다.
/// 중복 실행은 자동으로 건너뜁니다.
///
public static void WarmUp()
{
if (_warmupDone) return;
_warmupDone = true;
// 캐시 디렉터리에 이미 저장된 PNG 로드 (재시작 시 빠른 복원)
_ = Task.Run(() =>
{
try
{
Directory.CreateDirectory(_cacheDir);
foreach (var file in Directory.GetFiles(_cacheDir, "*.png"))
{
var key = Path.GetFileNameWithoutExtension(file);
_cache.TryAdd(key, file);
}
}
catch { }
});
// 자주 쓰는 확장자 사전 추출 (UI 스레드 부하를 분산하기 위해 순차 실행)
_ = Task.Run(async () =>
{
await Task.Delay(2000); // 앱 초기화 완료 후 추출 시작
var common = new (string Path, string Key, bool IsDir)[]
{
("dummy.exe", "exe", false),
("dummy.lnk", "lnk", false),
("dummy.pdf", "pdf", false),
("dummy.docx", "docx", false),
("dummy.xlsx", "xlsx", false),
("dummy.pptx", "pptx", false),
("dummy.txt", "txt", false),
("dummy.png", "png", false),
("dummy.zip", "zip", false),
("dummy.mp4", "mp4", false),
("C:\\", "folder", true),
};
foreach (var (path, key, isDir) in common)
{
if (_cache.ContainsKey(key)) continue;
await ExtractAsync(path, key, isDir);
await Task.Delay(80); // UI 스레드 부하 분산
}
});
}
// ─── 내부 ───────────────────────────────────────────────────────────────
private static string GetExtKey(string filePath)
{
var ext = Path.GetExtension(filePath).ToLowerInvariant();
return string.IsNullOrEmpty(ext) ? "noext" : ext.TrimStart('.');
}
private static async Task ExtractAsync(string filePath, string key, bool isDirectory)
{
if (_cache.ContainsKey(key)) return;
try
{
Directory.CreateDirectory(_cacheDir);
var cachePath = Path.Combine(_cacheDir, key + ".png");
// 파일이 이미 캐시에 있으면 메모리 캐시에 등록만
if (File.Exists(cachePath))
{
_cache.TryAdd(key, cachePath);
return;
}
// SHGetFileInfo + HICON → PNG 변환은 반드시 STA(Dispatcher) 스레드에서
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher == null) return;
var saved = await dispatcher.InvokeAsync(() =>
TryExtractAndSave(filePath, cachePath, isDirectory));
_cache.TryAdd(key, saved ? cachePath : null);
}
catch (Exception ex)
{
LogService.Warn($"[IconCache] '{key}' 추출 실패: {ex.Message}");
_cache.TryAdd(key, null);
}
}
/// Dispatcher(STA) 스레드에서 HICON을 PNG로 변환하여 저장합니다.
private static bool TryExtractAndSave(string filePath, string cachePath, bool isDirectory)
{
var info = new SHFILEINFO();
uint flags = SHGFI_ICON | SHGFI_LARGEICON | SHGFI_USEFILEATTRIBUTES;
uint attrs = isDirectory ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL;
SHGetFileInfo(filePath, attrs, ref info, (uint)Marshal.SizeOf(info), flags);
if (info.hIcon == IntPtr.Zero) return false;
try
{
var bitmap = Imaging.CreateBitmapSourceFromHIcon(
info.hIcon, Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
bitmap.Freeze();
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = File.OpenWrite(cachePath);
encoder.Save(stream);
return true;
}
catch
{
return false;
}
finally
{
DestroyIcon(info.hIcon);
}
}
}