From 0ea8b98f78f47ee1f933d6a9240291ad39aedf78 Mon Sep 17 00:00:00 2001 From: Vivor Loong Date: Tue, 23 Dec 2025 12:08:13 +0800 Subject: [PATCH 1/3] Cleanup 1.x: Removed QuickView, custom-ports, .github --- .github/ISSUE_TEMPLATE/bug_report.md | 38 - .github/ISSUE_TEMPLATE/feature_request.md | 20 - QuickView/AppStrings.h | 41 - QuickView/ContextMenu.cpp | 96 - QuickView/ContextMenu.h | 66 - QuickView/CoroutineTypes.h | 43 - QuickView/EditState.h | 107 - QuickView/FileNavigator.h | 102 - QuickView/GalleryOverlay.cpp | 362 -- QuickView/GalleryOverlay.h | 76 - QuickView/ImageLoader.cpp | 1978 ---------- QuickView/ImageLoader.h | 165 - QuickView/LosslessTransform.cpp | 489 --- QuickView/LosslessTransform.h | 79 - QuickView/OSDState.h | 29 - QuickView/PreviewExtractor.cpp | 244 -- QuickView/PreviewExtractor.h | 38 - QuickView/QuickView.h | 2 - QuickView/QuickView.rc | 8 - QuickView/QuickView.vcxproj | 145 - QuickView/RenderEngine.cpp | 399 -- QuickView/RenderEngine.h | 101 - QuickView/StbLoader.cpp | 113 - QuickView/StbLoader.h | 28 - QuickView/ThumbnailManager.cpp | 332 -- QuickView/ThumbnailManager.h | 121 - QuickView/TinyExrLoader.cpp | 86 - QuickView/TinyExrLoader.h | 18 - QuickView/Toolbar.cpp | 332 -- QuickView/Toolbar.h | 85 - QuickView/WuffsImpl.cpp | 383 -- QuickView/WuffsLoader.h | 74 - QuickView/build_error.txt | 13 - QuickView/build_error_log.txt | 44 - QuickView/build_fail_2.txt | 10 - QuickView/build_fail_round.txt | 4 - QuickView/build_output.txt | 29 - QuickView/framework.h | 13 - QuickView/main.cpp | 3357 ----------------- QuickView/pch.cpp | 1 - QuickView/pch.h | 37 - QuickView/revert_error.txt | 44 - QuickView/shim_error.txt | 44 - QuickView/targetver.h | 2 - QuickView/wrapper_error.txt | 44 - custom-ports/libpng/cmake.patch | 92 - custom-ports/libpng/libpng-config.cmake | 12 - custom-ports/libpng/portfile.cmake | 128 - custom-ports/libpng/usage | 4 - custom-ports/libpng/vcpkg-cmake-wrapper.cmake | 3 - custom-ports/libpng/vcpkg.json | 32 - custom-ports/zlib/portfile.cmake | 5 - custom-ports/zlib/vcpkg.json | 14 - 53 files changed, 10132 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 QuickView/AppStrings.h delete mode 100644 QuickView/ContextMenu.cpp delete mode 100644 QuickView/ContextMenu.h delete mode 100644 QuickView/CoroutineTypes.h delete mode 100644 QuickView/EditState.h delete mode 100644 QuickView/FileNavigator.h delete mode 100644 QuickView/GalleryOverlay.cpp delete mode 100644 QuickView/GalleryOverlay.h delete mode 100644 QuickView/ImageLoader.cpp delete mode 100644 QuickView/ImageLoader.h delete mode 100644 QuickView/LosslessTransform.cpp delete mode 100644 QuickView/LosslessTransform.h delete mode 100644 QuickView/OSDState.h delete mode 100644 QuickView/PreviewExtractor.cpp delete mode 100644 QuickView/PreviewExtractor.h delete mode 100644 QuickView/QuickView.h delete mode 100644 QuickView/QuickView.rc delete mode 100644 QuickView/QuickView.vcxproj delete mode 100644 QuickView/RenderEngine.cpp delete mode 100644 QuickView/RenderEngine.h delete mode 100644 QuickView/StbLoader.cpp delete mode 100644 QuickView/StbLoader.h delete mode 100644 QuickView/ThumbnailManager.cpp delete mode 100644 QuickView/ThumbnailManager.h delete mode 100644 QuickView/TinyExrLoader.cpp delete mode 100644 QuickView/TinyExrLoader.h delete mode 100644 QuickView/Toolbar.cpp delete mode 100644 QuickView/Toolbar.h delete mode 100644 QuickView/WuffsImpl.cpp delete mode 100644 QuickView/WuffsLoader.h delete mode 100644 QuickView/build_error.txt delete mode 100644 QuickView/build_error_log.txt delete mode 100644 QuickView/build_fail_2.txt delete mode 100644 QuickView/build_fail_round.txt delete mode 100644 QuickView/build_output.txt delete mode 100644 QuickView/framework.h delete mode 100644 QuickView/main.cpp delete mode 100644 QuickView/pch.cpp delete mode 100644 QuickView/pch.h delete mode 100644 QuickView/revert_error.txt delete mode 100644 QuickView/shim_error.txt delete mode 100644 QuickView/targetver.h delete mode 100644 QuickView/wrapper_error.txt delete mode 100644 custom-ports/libpng/cmake.patch delete mode 100644 custom-ports/libpng/libpng-config.cmake delete mode 100644 custom-ports/libpng/portfile.cmake delete mode 100644 custom-ports/libpng/usage delete mode 100644 custom-ports/libpng/vcpkg-cmake-wrapper.cmake delete mode 100644 custom-ports/libpng/vcpkg.json delete mode 100644 custom-ports/zlib/portfile.cmake delete mode 100644 custom-ports/zlib/vcpkg.json diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea78..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7d..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/QuickView/AppStrings.h b/QuickView/AppStrings.h deleted file mode 100644 index 73b773f7..00000000 --- a/QuickView/AppStrings.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -/// -/// Centralized string resources for easy localization -/// -namespace AppStrings { - // OSD Messages - static const wchar_t* OSD_NoImage = L"No image loaded"; - static const wchar_t* OSD_Lossless = L"Lossless"; - static const wchar_t* OSD_ReencodedLossless = L"Re-encoded (Lossless)"; - static const wchar_t* OSD_EdgeAdapted = L"Edge Adapted"; - static const wchar_t* OSD_Reencoded = L"Re-encoded"; - static const wchar_t* OSD_ReadOnly = L"Access denied - file may be in use or read-only"; - static const wchar_t* OSD_NotPerfect = L"Transform is not perfect (Edge optimized)"; - - // Transform Action Names - static const wchar_t* Action_RotateCW = L"Rotate 90\x00B0 CW"; - static const wchar_t* Action_RotateCCW = L"Rotate 90\x00B0 CCW"; - static const wchar_t* Action_Rotate180 = L"Rotate 180\x00B0"; - static const wchar_t* Action_FlipH = L"Flip Horizontal"; - static const wchar_t* Action_FlipV = L"Flip Vertical"; - - // Dialog Strings - static const wchar_t* Dialog_SaveTitle = L"Save Changes?"; - static const wchar_t* Dialog_SaveContent = L"The image has been modified. Do you want to save changes?"; - static const wchar_t* Dialog_ButtonSave = L"Save"; - static const wchar_t* Dialog_ButtonSaveAs = L"Save As..."; - static const wchar_t* Dialog_ButtonDiscard = L"Discard"; - - // Checkbox Labels - static const wchar_t* Checkbox_AlwaysSaveLossless = L"Always save lossless transforms"; - static const wchar_t* Checkbox_AlwaysSaveEdgeAdapted = L"Always save edge-adapted"; - static const wchar_t* Checkbox_AlwaysSaveLossy = L"Always save re-encoded"; - - // HEIC Codec Missing - static const wchar_t* OSD_HEICCodecMissing = L"Cannot decode HEIC - Install HEVC Video Extension"; - static const wchar_t* Dialog_HEICTitle = L"Cannot decode HEIC"; - static const wchar_t* Dialog_HEICContent = L"Your system is missing the HEVC Video Extension.\nQuickView uses system hardware acceleration for best performance."; - static const wchar_t* Dialog_HEICGetExtension = L"Get Extension (Free)"; - static const wchar_t* Dialog_Cancel = L"Cancel"; -} diff --git a/QuickView/ContextMenu.cpp b/QuickView/ContextMenu.cpp deleted file mode 100644 index 79f0ba09..00000000 --- a/QuickView/ContextMenu.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "pch.h" -#include "ContextMenu.h" -#include -#include - -// ============================================================ -// ContextMenu.cpp - Right-click Context Menu Implementation -// ============================================================ - -void ShowContextMenu(HWND hwnd, POINT pt, bool hasImage, bool needsExtensionFix, bool isWindowLocked, bool showInfoPanel, bool alwaysOnTop, bool renderRaw, bool isRawFile) { - HMENU hMenu = CreatePopupMenu(); - if (!hMenu) return; - - // ======================================================== - // [Open & Edit] Group - // ======================================================== - AppendMenuW(hMenu, MF_STRING, IDM_OPEN, L"Open...\tCtrl+O"); - AppendMenuW(hMenu, hasImage ? MF_STRING : MF_GRAYED, IDM_OPENWITH_DEFAULT, L"Open With..."); - AppendMenuW(hMenu, MF_STRING, IDM_EDIT, L"Edit (Default App)\tE"); - AppendMenuW(hMenu, MF_STRING, IDM_SHOW_IN_EXPLORER, L"Show in Explorer\tEnter"); - AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr); - AppendMenuW(hMenu, MF_STRING, IDM_COPY_IMAGE, L"Copy Image\tCtrl+C"); - AppendMenuW(hMenu, MF_STRING, IDM_COPY_PATH, L"Copy Path\tCtrl+Alt+C"); - AppendMenuW(hMenu, MF_STRING, IDM_PRINT, L"Print\tCtrl+P"); - AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr); - - // ======================================================== - // [Transform] Submenu - // ======================================================== - HMENU hTransformMenu = CreatePopupMenu(); - AppendMenuW(hTransformMenu, MF_STRING, IDM_ROTATE_CW, L"Rotate +90\xB0\tR"); // \xB0 is degree symbol - AppendMenuW(hTransformMenu, MF_STRING, IDM_ROTATE_CCW, L"Rotate -90\xB0\tShift+R"); - AppendMenuW(hTransformMenu, MF_STRING, IDM_FLIP_H, L"Flip Horizontal\tH"); - AppendMenuW(hTransformMenu, MF_STRING, IDM_FLIP_V, L"Flip Vertical\tV"); - - AppendMenuW(hMenu, hasImage ? MF_POPUP : (MF_POPUP | MF_GRAYED), (UINT_PTR)hTransformMenu, L"Transform"); - - // ======================================================== - // [View] Submenu - // ======================================================== - HMENU hViewMenu = CreatePopupMenu(); - AppendMenuW(hViewMenu, MF_STRING, IDM_ZOOM_100, L"Actual Size (100%)\t1 / Z"); - AppendMenuW(hViewMenu, MF_STRING, IDM_ZOOM_FIT, L"Fit to Screen\t0 / F"); - AppendMenuW(hViewMenu, MF_STRING, IDM_ZOOM_IN, L"Zoom In\t+ / Ctrl +"); - AppendMenuW(hViewMenu, MF_STRING, IDM_ZOOM_OUT, L"Zoom Out\t- / Ctrl -"); - AppendMenuW(hViewMenu, MF_SEPARATOR, 0, nullptr); - AppendMenuW(hViewMenu, MF_STRING | (isWindowLocked ? MF_CHECKED : 0), IDM_LOCK_WINDOW_SIZE, L"Lock Window Size"); - AppendMenuW(hViewMenu, MF_STRING | (alwaysOnTop ? MF_CHECKED : 0), IDM_ALWAYS_ON_TOP, L"Always on Top\tCtrl+T"); - AppendMenuW(hViewMenu, MF_SEPARATOR, 0, nullptr); - AppendMenuW(hViewMenu, MF_STRING, IDM_HUD_GALLERY, L"HUD Gallery\tT"); - - // Check states: - // Lite Info -> showInfoPanel && !infoPanelExpanded - AppendMenuW(hViewMenu, MF_STRING, IDM_LITE_INFO, L"Lite Info Panel\tTab"); - AppendMenuW(hViewMenu, MF_STRING | (showInfoPanel ? MF_CHECKED : 0), IDM_SHOW_INFO_PANEL, L"Show Info Panel\tI"); - AppendMenuW(hViewMenu, MF_SEPARATOR, 0, nullptr); - - UINT rawFlags = MF_STRING; - if (!isRawFile) rawFlags |= MF_GRAYED; - if (renderRaw) rawFlags |= MF_CHECKED; - AppendMenuW(hViewMenu, rawFlags, IDM_RENDER_RAW, L"Render RAW"); - - AppendMenuW(hViewMenu, MF_STRING, IDM_FULLSCREEN, L"Fullscreen\tF11"); - - AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hViewMenu, L"View"); - - // Set as Wallpaper submenu - HMENU hWallpaperMenu = CreatePopupMenu(); - AppendMenuW(hWallpaperMenu, MF_STRING, IDM_WALLPAPER_FILL, L"Fill"); - AppendMenuW(hWallpaperMenu, MF_STRING, IDM_WALLPAPER_FIT, L"Fit"); - AppendMenuW(hWallpaperMenu, MF_STRING, IDM_WALLPAPER_TILE, L"Tile"); - AppendMenuW(hMenu, hasImage ? MF_POPUP : (MF_POPUP | MF_GRAYED), (UINT_PTR)hWallpaperMenu, L"Set as Wallpaper"); - - AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr); - - // ======================================================== - // [File Operations] Group - // ======================================================== - AppendMenuW(hMenu, hasImage ? MF_STRING : MF_GRAYED, IDM_RENAME, L"Rename\tF2"); - AppendMenuW(hMenu, (hasImage && needsExtensionFix) ? MF_STRING : MF_GRAYED, IDM_FIX_EXTENSION, L"Fix Extension"); - AppendMenuW(hMenu, hasImage ? MF_STRING : MF_GRAYED, IDM_DELETE, L"Delete\tDel"); - AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr); - - // ======================================================== - // [Settings] Group - // ======================================================== - AppendMenuW(hMenu, MF_STRING, IDM_SETTINGS, L"Settings..."); - AppendMenuW(hMenu, MF_STRING, IDM_ABOUT, L"About QuickView"); - AppendMenuW(hMenu, MF_STRING, IDM_EXIT, L"Exit\tMButton/Esc"); - - // Show menu - TrackPopupMenu(hMenu, TPM_RIGHTBUTTON | TPM_LEFTALIGN | TPM_TOPALIGN, - pt.x, pt.y, 0, hwnd, nullptr); - - DestroyMenu(hMenu); -} diff --git a/QuickView/ContextMenu.h b/QuickView/ContextMenu.h deleted file mode 100644 index 3ee52eb8..00000000 --- a/QuickView/ContextMenu.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once -// ============================================================ -// ContextMenu.h - Right-click Context Menu Definitions -// ============================================================ - -#include - -// ============================================================ -// Command IDs (WM_COMMAND wParam) -// ============================================================ -enum ContextMenuCommand : UINT { - // [打开 & 编辑] Group - IDM_OPEN = 1001, - IDM_OPENWITH_DEFAULT, - IDM_EDIT, // Open with default editor - IDM_SHOW_IN_EXPLORER, - IDM_COPY_IMAGE, - IDM_COPY_PATH, - IDM_PRINT, - - // [视图控制] Group - IDM_FULLSCREEN, - IDM_ZOOM_100, - IDM_ZOOM_FIT, // Lite - IDM_ZOOM_IN, - IDM_ZOOM_OUT, - IDM_LOCK_WINDOW_SIZE, - IDM_SHOW_INFO_PANEL, // Full - IDM_LITE_INFO, // Lite - IDM_ALWAYS_ON_TOP, - IDM_RENDER_RAW, // Sync with toolbar - IDM_HUD_GALLERY, - IDM_WALLPAPER_FILL, - IDM_WALLPAPER_FIT, - IDM_WALLPAPER_TILE, - - // [Transform] Group - IDM_ROTATE_CW, - IDM_ROTATE_CCW, - IDM_FLIP_H, - IDM_FLIP_V, - - // [文件æ“�作] Group - IDM_RENAME, - IDM_FIX_EXTENSION, - IDM_DELETE, - - // [应用设置] Group - IDM_SETTINGS, - IDM_ABOUT, - IDM_EXIT, -}; - -/// -/// Show context menu at specified screen position -/// -/// Parent window handle -/// Screen coordinates for menu position -/// Whether an image is currently loaded -/// Whether extension fix is available -/// Whether window size is locked -/// Whether info panel is shown -/// Whether window is always on top -/// Whether Render RAW mode is active -/// Whether current file is RAW format -void ShowContextMenu(HWND hwnd, POINT pt, bool hasImage, bool needsExtensionFix, bool isWindowLocked, bool showInfoPanel, bool alwaysOnTop, bool renderRaw, bool isRawFile); diff --git a/QuickView/CoroutineTypes.h b/QuickView/CoroutineTypes.h deleted file mode 100644 index 940670a6..00000000 --- a/QuickView/CoroutineTypes.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include -#include -#include -#include -#define WIN32_LEAN_AND_MEAN -#include - -// Simple Fire-and-Forget coroutine type for top-level calls (e.g. from event handlers) -struct FireAndForget { - struct promise_type { - FireAndForget get_return_object() { return {}; } - std::suspend_never initial_suspend() { return {}; } - std::suspend_never final_suspend() noexcept { return {}; } - void return_void() {} - void unhandled_exception() { std::terminate(); } // Verify: Real apps might want to log - }; -}; - -// Awaitable to switch to a background thread -struct ResumeBackground { - bool await_ready() const { return false; } - void await_suspend(std::coroutine_handle<> h) { - std::thread([h]() { h.resume(); }).detach(); - } - void await_resume() {} -}; - -// Helper to switch to main thread (requires HWND) -struct ResumeMainThread { - HWND m_hwnd; - ResumeMainThread(HWND hwnd) : m_hwnd(hwnd) {} - - bool await_ready() const { return false; } - void await_suspend(std::coroutine_handle<> h) { - // Post a custom message or use a thread-safe queue. - // For simplicity in this project, we can use PostMessage with a callback? - // Actually, Win32 message loop needs a way to execute this. - // Let's use a custom message WM_APP + 1 for "Execute Coroutine Handle" - PostMessage(m_hwnd, WM_APP + 1, 0, (LPARAM)h.address()); - } - void await_resume() {} -}; diff --git a/QuickView/EditState.h b/QuickView/EditState.h deleted file mode 100644 index 7bd952b7..00000000 --- a/QuickView/EditState.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once -#include "pch.h" -#include "LosslessTransform.h" // For EditQuality enum - -// EditQuality enum definition moved to LosslessTransform.h - -/// -/// Current edit state for non-destructive editing -/// -struct EditState { - bool IsDirty = false; // Has unsaved changes - std::wstring TempFilePath; // Temp file path - std::wstring OriginalFilePath; // Original file path - EditQuality Quality = EditQuality::Lossless; - int TotalRotation = 0; // Cumulative rotation (0/90/180/270) - bool FlippedH = false; // Horizontal flip state - bool FlippedV = false; // Vertical flip state - - void Reset() { - IsDirty = false; - TempFilePath.clear(); - OriginalFilePath.clear(); // Fix: Clear original path on reset - TotalRotation = 0; - FlippedH = false; - FlippedV = false; - Quality = EditQuality::Lossless; - } - - /// - /// Get color for OSD based on edit quality - /// - D2D1_COLOR_F GetQualityColor() const { - switch (Quality) { - case EditQuality::Lossless: - case EditQuality::LosslessReenc: - return D2D1::ColorF(0.1f, 0.6f, 0.1f, 0.9f); // Green - case EditQuality::EdgeAdapted: - return D2D1::ColorF(0.7f, 0.5f, 0.0f, 0.9f); // Yellow/Orange - case EditQuality::Lossy: - return D2D1::ColorF(0.7f, 0.2f, 0.1f, 0.9f); // Red - default: - return D2D1::ColorF(0.3f, 0.3f, 0.3f, 0.9f); // Gray - } - } - - /// - /// Get quality description for OSD - /// - const wchar_t* GetQualityText() const { - switch (Quality) { - case EditQuality::Lossless: return L"Lossless"; - case EditQuality::LosslessReenc: return L"Re-encoded (Lossless)"; - case EditQuality::EdgeAdapted: return L"Edge Adapted"; - case EditQuality::Lossy: return L"Re-encoded"; - default: return L""; - } - } -}; - -enum class MouseAction { - None, WindowDrag, PanImage, ExitApp, FitWindow -}; - -/// -/// Application configuration (for future settings menu) -/// -struct AppConfig { - bool AutoSaveOnSwitch = false; // Auto-save when switching images - bool AlwaysSaveLossless = false; // Always save lossless transforms - bool AlwaysSaveEdgeAdapted = false; // Always save edge-adapted transforms - bool AlwaysSaveLossy = false; // Always save lossy transforms - bool ShowSavePrompt = true; // Show save prompt - float DialogAlpha = 0.95f; // OSD Dialog transparency - - // Interaction - bool ResizeWindowOnZoom = true; - bool RenderRAW = false; // Sync RAW preview state - MouseAction LeftDragAction = MouseAction::WindowDrag; - MouseAction MiddleDragAction = MouseAction::PanImage; - MouseAction MiddleClickAction = MouseAction::ExitApp; - bool AutoHideWindowControls = true; - - // View options - bool LockWindowSize = false; // Prevent window auto-resize - bool ShowInfoPanel = false; // Master Toggle - bool InfoPanelExpanded = false; // Compact vs Full - bool ForceRawDecode = false; // Force full RAW decoding (slower but high res) - bool AlwaysOnTop = false; // Window always on top - - /// - /// Should auto-save for given quality? - /// - bool ShouldAutoSave(EditQuality quality) const { - if (AutoSaveOnSwitch) return true; - switch (quality) { - case EditQuality::Lossless: - case EditQuality::LosslessReenc: - return AlwaysSaveLossless; - case EditQuality::EdgeAdapted: - return AlwaysSaveEdgeAdapted; - case EditQuality::Lossy: - return AlwaysSaveLossy; - default: - return false; - } - } -}; diff --git a/QuickView/FileNavigator.h b/QuickView/FileNavigator.h deleted file mode 100644 index 2ded2926..00000000 --- a/QuickView/FileNavigator.h +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -class FileNavigator { -public: - void Initialize(const std::wstring& currentPath) { - namespace fs = std::filesystem; - fs::path p(currentPath); - if (!fs::exists(p)) return; - - m_files.clear(); - m_currentIndex = -1; - - // Parent directory - fs::path dir = p.parent_path(); - if (dir.empty()) return; - - // Supported extensions (Basic list for now) - const std::vector extensions = { L".jpg", L".jpeg", L".png", L".webp", L".bmp", L".gif", L".heic", L".avif" }; - - try { - for (const auto& entry : fs::directory_iterator(dir)) { - if (entry.is_regular_file()) { - std::wstring ext = entry.path().extension().wstring(); - std::transform(ext.begin(), ext.end(), ext.begin(), [](wchar_t c){ return std::towlower(c); }); - - for (const auto& supp : extensions) { - if (ext == supp) { - m_files.push_back(entry.path().wstring()); - break; - } - } - } - } - } catch (...) {} - - // Natural Sort would be better, but lexicon is fine for start - std::sort(m_files.begin(), m_files.end()); - - // Find current index - std::wstring currentFull = p.wstring(); - for (size_t i = 0; i < m_files.size(); ++i) { - if (m_files[i] == currentFull) { - m_currentIndex = (int)i; - break; - } - } - } - - std::wstring Next() { - if (m_files.empty()) return L""; - m_currentIndex = (m_currentIndex + 1) % m_files.size(); - return m_files[m_currentIndex]; - } - - std::wstring Previous() { - if (m_files.empty()) return L""; - m_currentIndex = (m_currentIndex - 1 + m_files.size()) % m_files.size(); - return m_files[m_currentIndex]; - } - - std::wstring PeekNext() const { - if (m_files.empty()) return L""; - size_t nextIdx = (m_currentIndex + 1) % m_files.size(); - return m_files[nextIdx]; - } - - std::wstring PeekPrevious() const { - if (m_files.empty()) return L""; - size_t prevIdx = (m_currentIndex - 1 + m_files.size()) % m_files.size(); - return m_files[prevIdx]; - } - - // Status info - // Status info - size_t Count() const { return m_files.size(); } - int Index() const { return m_currentIndex; } - - // Random Access (For Gallery Virtualization) - const std::wstring& GetFile(int index) const { - static std::wstring empty; - if (index < 0 || index >= (int)m_files.size()) return empty; - return m_files[index]; - } - - int FindIndex(const std::wstring& path) const { - auto it = std::find(m_files.begin(), m_files.end(), path); - if (it != m_files.end()) return (int)std::distance(m_files.begin(), it); - return -1; - } - - const std::vector& GetAllFiles() const { return m_files; } - - -private: - std::vector m_files; - int m_currentIndex = -1; -}; diff --git a/QuickView/GalleryOverlay.cpp b/QuickView/GalleryOverlay.cpp deleted file mode 100644 index bb8e2fb5..00000000 --- a/QuickView/GalleryOverlay.cpp +++ /dev/null @@ -1,362 +0,0 @@ -#include "pch.h" -#include "GalleryOverlay.h" -#include "ThumbnailManager.h" -#include "FileNavigator.h" -#include -#include - -GalleryOverlay::GalleryOverlay() {} - -GalleryOverlay::~GalleryOverlay() {} - -void GalleryOverlay::Initialize(ThumbnailManager* pThumbMgr, FileNavigator* pNav) { - m_pThumbMgr = pThumbMgr; - m_pNav = pNav; -} - -void GalleryOverlay::Open(int currentIndex) { - if (!m_pNav || m_pNav->Count() == 0) return; - - m_isVisible = true; - m_opacity = 0.0f; // Start fade in - m_selectedIndex = currentIndex; - - // Smart Reveal: Calculate Scroll Top to center the current item - // We need layout info (cell width/height). But we might not have Render size yet. - // Assuming Render gets called soon. We can set target index and handle scrolling in Render? - // Or just estimating layout if we know window size. - // Let's defer exact scroll calculation to first Render if size is unknown? - // Actually, we can pre-calc if we assume full window. But Render passes size. - // Let's set a flag "FocusOnNextRender" or handle it in Render logic if first frame. - // For now, let's just do a best guess or force EnsureVisible logic in Render. - // Actually, OnPaint calls Render. -} - -void GalleryOverlay::Close() { - m_isVisible = false; - if (m_pThumbMgr) m_pThumbMgr->ClearQueue(); // Stop loading -} - -void GalleryOverlay::Update(float deltaTime) { - if (m_isVisible && m_opacity < 1.0f) { - m_opacity += deltaTime * 5.0f; // 0.2s fade in - if (m_opacity > 1.0f) m_opacity = 1.0f; - } -} - -void GalleryOverlay::EnsureVisible(int index) { - if (index < 0 || m_cellHeight <= 0) return; - - int row = index / m_cols; - float itemTop = PADDING + row * (m_cellHeight + GAP); - float itemBottom = itemTop + m_cellHeight; - - // Current Viewport - float viewTop = m_scrollTop; - float viewBottom = m_scrollTop + m_totalHeight; // Wait, m_totalHeight is content height. ViewHeight is screen height. - // We need screen height. - // Let's rely on Render to clamp. - // Here we just adjust m_scrollTop. - // We need D2D size here? - // Let's store last known viewport height. -} - -// Helper to center crop -static D2D1_RECT_F GetCenterCropRect(D2D1_SIZE_F imgSize, D2D1_RECT_F destRect) { - float imgRatio = imgSize.width / imgSize.height; - float destW = destRect.right - destRect.left; - float destH = destRect.bottom - destRect.top; - float destRatio = destW / destH; - - D2D1_RECT_F srcRect; - if (imgRatio > destRatio) { - // Image is wider, crop sides - float scale = imgSize.height / destH; - float cropW = destW * scale; - float offset = (imgSize.width - cropW) / 2.0f; - srcRect = D2D1::RectF(offset, 0, offset + cropW, imgSize.height); - } else { - // Image is taller, crop top/bottom - float scale = imgSize.width / destW; - float cropH = destH * scale; - float offset = (imgSize.height - cropH) / 2.0f; - srcRect = D2D1::RectF(0, offset, imgSize.width, offset + cropH); - } - return srcRect; -} - -void GalleryOverlay::Render(ID2D1DeviceContext* pDC, const D2D1_SIZE_F& size) { - if (!m_isVisible || !m_pThumbMgr || !m_pNav) return; - - // Init resources - if (!m_brushBg) pDC->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.85f), &m_brushBg); - if (!m_brushSelection) pDC->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::DodgerBlue), &m_brushSelection); // Accent - if (!m_brushText) pDC->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &m_brushText); - - // Background - D2D1_RECT_F screenRect = D2D1::RectF(0, 0, size.width, size.height); - m_brushBg->SetOpacity(m_opacity * 0.85f); - pDC->FillRectangle(screenRect, m_brushBg.Get()); - - // Init Text Resources (Lazy) - if (!m_dwriteFactory) { - DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(m_dwriteFactory.GetAddressOf())); - } - if (m_dwriteFactory && !m_textFormat) { - m_dwriteFactory->CreateTextFormat(L"Consolas", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 12.0f, L"en-us", &m_textFormat); - m_dwriteFactory->CreateTextFormat(L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 20.0f, L"en-us", &m_textFormatOSD); - } - - size_t count = m_pNav->Count(); - if (count == 0) return; - - // Layout Calculation - float availWidth = size.width - PADDING * 2; - // Calculate cols: Adaptive. Min 180px per col. - m_cols = std::max(1, (int)(availWidth / (THUMB_SIZE_MIN + GAP))); - - // Refine cell width to fill space - // availWidth = cols * w + (cols-1) * gap - m_cellWidth = (availWidth - (m_cols - 1) * GAP) / m_cols; - m_cellHeight = m_cellWidth * 0.75f; // 4:3 Aspect (User suggested 1:1 or 4:3. 4:3 is nice for photo) - // Actually user said "Square (1:1) or 4:3". Let's use 1:1 for "Geek" look (Square Grid) - m_cellHeight = m_cellWidth; - - int rows = (int)((count + m_cols - 1) / m_cols); - m_totalHeight = PADDING * 2 + rows * (m_cellHeight + GAP) - GAP; - m_maxScroll = std::max(0.0f, m_totalHeight - size.height); - - // Smart Reveal (First Frame Logic) - static bool firstFrame = true; // Use member if re-opening needs this. m_opacity < 0.1f implies just opened. - if (m_opacity < 0.2f && m_selectedIndex >= 0) { // Approx first few frames - int row = m_selectedIndex / m_cols; - float targetY = PADDING + row * (m_cellHeight + GAP); - // Center it - m_scrollTop = targetY - size.height / 2.0f + m_cellHeight / 2.0f; - // Clamp - if (m_scrollTop < 0) m_scrollTop = 0; - if (m_scrollTop > m_maxScroll) m_scrollTop = m_maxScroll; - } - - // Virtualization Range - // Visible Y range: m_scrollTop to m_scrollTop + size.height - int startRow = std::max(0, (int)((m_scrollTop - PADDING) / (m_cellHeight + GAP))); - int endRow = std::min(rows - 1, (int)((m_scrollTop + size.height - PADDING) / (m_cellHeight + GAP)) + 1); - - int startIdx = startRow * m_cols; - int endIdx = std::min((int)count - 1, (endRow + 1) * m_cols - 1); - - int centerIdx = (startIdx + endIdx) / 2; - - // Notify Manager to clear old tasks - m_pThumbMgr->UpdateOptimizedPriority(startIdx, endIdx, centerIdx); - - pDC->SetTransform(D2D1::Matrix3x2F::Translation(0, -m_scrollTop)); - m_brushBg->SetOpacity(m_opacity); // Reset specific opacity for render? Back to 1 for content? - // Actually content opacity should match m_opacity. - - // Loop Visible Items - for (int i = startIdx; i <= endIdx; ++i) { - int r = i / m_cols; - int c = i % m_cols; - - float x = PADDING + c * (m_cellWidth + GAP); - float y = PADDING + r * (m_cellHeight + GAP); - D2D1_RECT_F cellRect = D2D1::RectF(x, y, x + m_cellWidth, y + m_cellHeight); - - // Selection Highlight - if (i == m_selectedIndex) { - D2D1_RECT_F highlight = D2D1::RectF(x-4, y-4, x+m_cellWidth+4, y+m_cellHeight+4); - pDC->FillRoundedRectangle(D2D1::RoundedRect(highlight, 4, 4), m_brushSelection.Get()); - } - - // Get Thumbnail - const std::wstring& path = m_pNav->GetFile(i); - auto bmp = m_pThumbMgr->GetThumbnail(i, path.c_str(), pDC); - - if (bmp) { - D2D1_SIZE_F bmpSize = bmp->GetSize(); - D2D1_RECT_F src = GetCenterCropRect(bmpSize, cellRect); - pDC->DrawBitmap(bmp.Get(), cellRect, m_opacity, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, src); - } else { - // Placeholder (Gray Box) call QueueRequest - D2D1_COLOR_F color = D2D1::ColorF(0.2f, 0.2f, 0.2f, m_opacity); - ComPtr phBrush; - pDC->CreateSolidColorBrush(color, &phBrush); - pDC->FillRectangle(cellRect, phBrush.Get()); - - // Queue! (Priority based on distance to center) - int prio = std::abs(i - centerIdx); - m_pThumbMgr->QueueRequest(i, path.c_str(), prio); - - // Draw "Loading..." ? Optional. - } - - // Draw Info on Hover (User request) - if (m_hoverIndex == i && m_textFormat) { - // Draw Hover items on top of EVERYTHING? - // Actually inside loop is fine if z-order is managed, but tooltips should be top-most. - // We can defer drawing or just assume loop order (later items cover previous). - // But if I hover item 5, item 6 might overlap tooltip? - // Not if tooltip is drawn AFTER loop. - } - } - - // Draw Hover Tooltip (Top-most) - if (m_hoverIndex >= startIdx && m_hoverIndex <= endIdx) { // Visible check - int r = m_hoverIndex / m_cols; - int c = m_hoverIndex % m_cols; - float x = PADDING + c * (m_cellWidth + GAP); - float y = PADDING + r * (m_cellHeight + GAP); - - ThumbnailManager::ImageInfo info = m_pThumbMgr->GetImageInfo(m_hoverIndex); - std::wstring path = m_pNav->GetFile(m_hoverIndex); - size_t lastSlash = path.find_last_of(L"\\/"); - std::wstring filename = (lastSlash != std::wstring::npos) ? path.substr(lastSlash + 1) : path; - - std::wstring desc = filename + L"\n"; - if (info.isValid) { - desc += std::to_wstring(info.origWidth) + L" x " + std::to_wstring(info.origHeight) + L"\n"; - desc += std::to_wstring(info.fileSize / 1024) + L" KB"; - } else { - desc += L"Loading..."; - } - - // Measure text rough layout - float tooltipW = 200.0f; - float tooltipH = 60.0f; // Approx - - D2D1_RECT_F tooltipRect = D2D1::RectF(x + 10, y + 10, x + 10 + tooltipW, y + 10 + tooltipH); - - // Ensure inside screen (optional, skipping for brevity) - - m_brushBg->SetOpacity(0.9f); - pDC->FillRoundedRectangle(D2D1::RoundedRect(tooltipRect, 4, 4), m_brushBg.Get()); - - m_brushText->SetOpacity(1.0f); - pDC->DrawText(desc.c_str(), (UINT32)desc.length(), m_textFormat.Get(), - D2D1::RectF(tooltipRect.left + 5, tooltipRect.top + 5, tooltipRect.right - 5, tooltipRect.bottom - 5), - m_brushText.Get()); - } - - pDC->SetTransform(D2D1::Matrix3x2F::Identity()); -} - -bool GalleryOverlay::OnMouseWheel(int delta) { - if (!m_isVisible) return false; - float scrollSpeed = 60.0f; // Pixels per detent - m_scrollTop -= (delta / 120.0f) * scrollSpeed * 3.0f; // 3x speed for "Smooth/Fast" feel - - if (m_scrollTop < 0) m_scrollTop = 0; - if (m_scrollTop > m_maxScroll) m_scrollTop = m_maxScroll; - return true; -} - -bool GalleryOverlay::OnKeyDown(UINT key) { - if (!m_isVisible) return false; - - switch (key) { - case VK_ESCAPE: - case 'T': - Close(); - return true; - - case VK_RETURN: - if (m_selectedIndex >= 0) Close(); // Main window should pick up selected index from Navigator? - // Wait, we need to sync Navigator! - // Close() checks m_selectedIndex but we need access to change Navigator index. - // Navigator doesn't allow setting arbitrary index easily? - // FileNavigator::Initialize(path)? - // Or add FileNavigator::SetIndex? - // The user plan said "Close and jump". - // We should ensure Navigator is updated. - return true; - - case VK_LEFT: m_selectedIndex = std::max(0, m_selectedIndex - 1); break; - case VK_RIGHT: m_selectedIndex = std::min((int)m_pNav->Count() - 1, m_selectedIndex + 1); break; - case VK_UP: m_selectedIndex = std::max(0, m_selectedIndex - m_cols); break; - case VK_DOWN: m_selectedIndex = std::min((int)m_pNav->Count() - 1, m_selectedIndex + m_cols); break; - - case VK_PRIOR: // PgUp - m_selectedIndex = std::max(0, m_selectedIndex - m_cols * 5); // Jump 5 rows - break; - case VK_NEXT: // PgDown - m_selectedIndex = std::min((int)m_pNav->Count() - 1, m_selectedIndex + m_cols * 5); - break; - - case VK_HOME: m_selectedIndex = 0; break; - case VK_END: m_selectedIndex = (int)m_pNav->Count() - 1; break; - } - - // Auto-scroll to selection - if (m_selectedIndex >= 0) { - int row = m_selectedIndex / m_cols; - float itemTop = PADDING + row * (m_cellHeight + GAP); - float itemBottom = itemTop + m_cellHeight; - - // Assume screen height (need to track it or guess) - // Best effort: we need to persist viewHeight to do this logic here. - // Let's assume OnMouseWheel/Render updates m_maxScroll logic using viewHeight, - // but we don't have it here. We can just invalidate and let specific EnsureVisible logic defined in Render/Update handle it? - // Or better: In Render, if selected is out of view, scroll to it? - // Let's implement simple "Scroll To Selection" in Render if input changed selection. - } - return true; -} - -bool GalleryOverlay::OnLButtonDown(int x, int y) { - if (!m_isVisible) return false; - int idx = HitTest((float)x, (float)y + m_scrollTop); // Adjust for scroll - if (idx >= 0) { - m_selectedIndex = idx; - Close(); // Confirm selection - return true; - } - // Click outside = Close? - Close(); - return true; -} - -bool GalleryOverlay::OnMouseMove(int x, int y) { - if (!m_isVisible) return false; - m_hoverIndex = HitTest((float)x, (float)y + m_scrollTop); - return true; // Consume? maybe not? -} - -int GalleryOverlay::HitTest(float x, float y) { - // Inverse of layout - // y = PADDING + r * (h + GAP) - // r = (y - PADDING) / (h + GAP) - if (x < PADDING || y < PADDING) return -1; - - float rowF = (y - PADDING) / (m_cellHeight + GAP); - float colF = (x - PADDING) / (m_cellWidth + GAP); - - int r = (int)rowF; - int c = (int)colF; - - // Check gaps (fractional part) - float rowFrac = rowF - r; - float colFrac = colF - c; - - // If in gap (GAP is small, but strictly speaking) - // Just ignore gap precision for easier clicking? Or be strict. - // Strict is cleaner. - // GAP relative to total stride: - // Stride = Cell + GAP. - // If local pos within stride > Cell, it's gap. - - // Simpler check: Index - if (c >= m_cols) return -1; - - int index = r * m_cols + c; - if (index >= (int)m_pNav->Count()) return -1; - - // Check rect - float cellR = PADDING + c * (m_cellWidth + GAP) + m_cellWidth; - float cellB = PADDING + r * (m_cellHeight + GAP) + m_cellHeight; - - if (x > cellR || y > cellB) return -1; // In gap - - return index; -} diff --git a/QuickView/GalleryOverlay.h b/QuickView/GalleryOverlay.h deleted file mode 100644 index eb638086..00000000 --- a/QuickView/GalleryOverlay.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once -#include "pch.h" -#include "ThumbnailManager.h" -#include "FileNavigator.h" - -// Forward declaration if needed, but we include headers. -// We need ID2D1DeviceContext - -class GalleryOverlay { -public: - GalleryOverlay(); - ~GalleryOverlay(); - - void Initialize(ThumbnailManager* pThumbMgr, FileNavigator* pNav); - - // Render the gallery overlay - void Render(ID2D1DeviceContext* pDC, const D2D1_SIZE_F& size); - - // Interaction - bool OnKeyDown(UINT key); // Returns true if handled - bool OnMouseWheel(int delta); - bool OnLButtonDown(int x, int y); - bool OnMouseMove(int x, int y); - - // State Control - void Open(int currentIndex); - void Close(); - bool IsVisible() const { return m_isVisible; } - float GetOpacity() const { return m_opacity; } - - // Returns selected index when closed via Enter/Click - int GetSelectedIndex() const { return m_selectedIndex; } - - // Animation tick (fade in) - void Update(float deltaTime); - -private: - ThumbnailManager* m_pThumbMgr = nullptr; - FileNavigator* m_pNav = nullptr; - - bool m_isVisible = false; - float m_opacity = 0.0f; // 0.0 - 1.0 (Fade in) - - // Scroll state - float m_scrollTop = 0.0f; - float m_maxScroll = 0.0f; - float m_targetScrollTop = 0.0f; // For smooth scroll if needed (simplified to direct scroll for now) - - // Layout - int m_cols = 6; - float m_cellWidth = 0.0f; - float m_cellHeight = 0.0f; // Square cells - float m_totalHeight = 0.0f; - - int m_selectedIndex = -1; - int m_hoverIndex = -1; - - // Constants - const float GAP = 12.0f; - const float PADDING = 40.0f; // Outer padding - const float THUMB_SIZE_MIN = 180.0f; // Adaptive sizing - - // Resources - ComPtr m_brushBg; - ComPtr m_brushSelection; - ComPtr m_brushText; - - // Text Rendering - ComPtr m_dwriteFactory; - ComPtr m_textFormat; // For Grid cells - ComPtr m_textFormatOSD; // For global stats - ComPtr m_brushTextBg; - - void EnsureVisible(int index); - int HitTest(float x, float y); -}; diff --git a/QuickView/ImageLoader.cpp b/QuickView/ImageLoader.cpp deleted file mode 100644 index 6f68fe99..00000000 --- a/QuickView/ImageLoader.cpp +++ /dev/null @@ -1,1978 +0,0 @@ -#include "pch.h" - -// NanoSVG -#pragma warning(push) -#pragma warning(disable: 4244) -#pragma warning(disable: 4702) -#define NANOSVG_IMPLEMENTATION -#define NANOSVGRAST_IMPLEMENTATION -#include "../third_party/nanosvg/nanosvg.h" -#include "../third_party/nanosvg/nanosvgrast.h" -#pragma warning(pop) -#include "ImageLoader.h" -#include // JXL -#include "WuffsLoader.h" -#include "StbLoader.h" -#include "TinyExrLoader.h" -#include -#include "PreviewExtractor.h" - -// Helper to detect format from file content (Magic Bytes) -static std::wstring DetectFormatFromContent(LPCWSTR filePath) { - // 1. Read first 16 bytes for Magic Number - uint8_t magic[16] = {0}; - bool magicRead = false; - { - HANDLE hFile = CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile != INVALID_HANDLE_VALUE) { - DWORD bytesRead = 0; - if (ReadFile(hFile, magic, 16, &bytesRead, nullptr) && bytesRead >= 4) { - magicRead = true; - } - CloseHandle(hFile); - } - } - - if (!magicRead) return L"Unknown"; - - // Check JPEG: FF D8 FF - if (magic[0] == 0xFF && magic[1] == 0xD8 && magic[2] == 0xFF) return L"JPEG"; - - // Check PNG: 89 50 4E 47 - if (magic[0] == 0x89 && magic[1] == 0x50 && magic[2] == 0x4E && magic[3] == 0x47) return L"PNG"; - - // Check WebP: RIFF ... WEBP - if (magic[0] == 'R' && magic[1] == 'I' && magic[2] == 'F' && magic[3] == 'F' && - magic[8] == 'W' && magic[9] == 'E' && magic[10] == 'B' && magic[11] == 'P') return L"WebP"; - - // Check AVIF: ftypavif - if (magic[4] == 'f' && magic[5] == 't' && magic[6] == 'y' && magic[7] == 'p' && - magic[8] == 'a' && magic[9] == 'v' && magic[10] == 'i' && magic[11] == 'f') return L"AVIF"; - - // Check HEIC/HEIF: ftyp + brand - // Common brands: heic, heix, hevc, heim, heis, hevm, hevs, mif1, msf1 - if (magic[4] == 'f' && magic[5] == 't' && magic[6] == 'y' && magic[7] == 'p') { - // Check brand at offset 8 - if ((magic[8] == 'h' && magic[9] == 'e' && magic[10] == 'i' && (magic[11] == 'c' || magic[11] == 'x' || magic[11] == 's' || magic[11] == 'm')) || // heic, heix, heis, heim - (magic[8] == 'h' && magic[9] == 'e' && magic[10] == 'v' && (magic[11] == 'c' || magic[11] == 'm' || magic[11] == 's')) || // hevc, hevm, hevs - (magic[8] == 'm' && magic[9] == 'i' && magic[10] == 'f' && magic[11] == '1') || // mif1 - (magic[8] == 'm' && magic[9] == 's' && magic[10] == 'f' && magic[11] == '1')) // msf1 - { - return L"HEIC"; // Unified as HEIC/HEIF - } - } - - // Check JXL: FF 0A or 00 00 00 0C JXL - if (magic[0] == 0xFF && magic[1] == 0x0A) return L"JXL"; - if (magic[0] == 0x00 && magic[1] == 0x00 && magic[2] == 0x00 && magic[3] == 0x0C && - magic[4] == 'J' && magic[5] == 'X' && magic[6] == 'L' && magic[7] == ' ') return L"JXL"; - - // Check GIF: GIF87a or GIF89a - if (magic[0] == 'G' && magic[1] == 'I' && magic[2] == 'F' && magic[3] == '8' && - (magic[4] == '7' || magic[4] == '9') && magic[5] == 'a') return L"GIF"; - - // Check BMP: BM - if (magic[0] == 'B' && magic[1] == 'M') return L"BMP"; - - // Check PSD: 8BPS - if (magic[0] == '8' && magic[1] == 'B' && magic[2] == 'P' && magic[3] == 'S') return L"PSD"; - - // Check HDR: #?RADIANCE or #?RGBE - if (magic[0] == '#' && magic[1] == '?') return L"HDR"; - - // Check EXR: v/1\x01 (0x76 0x2f 0x31 0x01) - if (magic[0] == 0x76 && magic[1] == 0x2F && magic[2] == 0x31 && magic[3] == 0x01) return L"EXR"; - - // Check PIC: 0x53 0x80 ... - if (magic[0] == 0x53 && magic[1] == 0x80 && magic[2] == 0xF6 && magic[3] == 0x34) return L"PIC"; - - // Check PNM: P1-P7 - if (magic[0] == 'P' && magic[1] >= '1' && magic[1] <= '7') return L"PNM"; - - // Check QOI: qoif - if (magic[0] == 'q' && magic[1] == 'o' && magic[2] == 'i' && magic[3] == 'f') return L"QOI"; - - // Check PCX: 0x0A ... - if (magic[0] == 0x0A) return L"PCX"; - - // Check ICO: 00 00 01 00 - if (magic[0] == 0x00 && magic[1] == 0x00 && magic[2] == 0x01 && magic[3] == 0x00) return L"ICO"; - - // Check TIFF: II (49 49 2A 00) or MM (4D 4D 00 2A) - if (magic[0] == 0x49 && magic[1] == 0x49 && magic[2] == 0x2A && magic[3] == 0x00) return L"TIFF"; - if (magic[0] == 0x4D && magic[1] == 0x4D && magic[2] == 0x00 && magic[3] == 0x2A) return L"TIFF"; - - return L"Unknown"; -} - -HRESULT CImageLoader::Initialize(IWICImagingFactory* wicFactory) { - if (!wicFactory) return E_INVALIDARG; - m_wicFactory = wicFactory; - return S_OK; -} - -HRESULT CImageLoader::LoadFromFile(LPCWSTR filePath, IWICBitmapSource** bitmap) { - if (!filePath || !bitmap) return E_INVALIDARG; - - // Delegate to the new architecture (Detected Logic + Specialized Loaders + WIC Fallback) - IWICBitmap* pBitmap = nullptr; - std::wstring loaderName; // Optional, or ignore - - // We cast IWICBitmap** to IWICBitmapSource**? - // No, LoadToMemory returns IWICBitmap. We want IWICBitmapSource. - // IWICBitmap* supports IWICBitmapSource interface. - // But pointers are different? No, inheritance. - - // Actually LoadToMemory signature: - // HRESULT LoadToMemory(LPCWSTR filePath, IWICBitmap** ppBitmap, std::wstring* pLoaderName = nullptr); - - ComPtr ptrBitmap; - HRESULT hr = LoadToMemory(filePath, &ptrBitmap, nullptr); - - if (SUCCEEDED(hr)) { - *bitmap = ptrBitmap.Detach(); // Detach gives strictly the pointer. - return S_OK; - } - - return hr; -} - -#include - -// High-Performance Library Includes -// libpng REMOVED - replaced by Wuffs -#include // libwebp -#include -#include // libavif -#include // libjxl -#include -#include -#include // libraw - -#include -#include -#include -#include // For hardware_concurrency - -// Wuffs (Google's memory-safe decoder) -// Implementation is in WuffsImpl.cpp with selective module loading -#include "WuffsLoader.h" - -// Global storage for format details (set by loaders, read by main) -static std::wstring g_lastFormatDetails; - -// Helper to read file to vector -bool ReadFileToVector(LPCWSTR filePath, std::vector& buffer) { - HANDLE hFile = CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile == INVALID_HANDLE_VALUE) return false; - - DWORD fileSize = GetFileSize(hFile, nullptr); - if (fileSize == INVALID_FILE_SIZE) { CloseHandle(hFile); return false; } - - buffer.resize(fileSize); - DWORD bytesRead; - BOOL result = ReadFile(hFile, buffer.data(), fileSize, &bytesRead, nullptr); - CloseHandle(hFile); - return result && bytesRead == fileSize; -} - -HRESULT CImageLoader::CreateWICBitmapFromMemory(UINT width, UINT height, REFGUID format, UINT stride, UINT size, BYTE* data, IWICBitmap** ppBitmap) { - if (!m_wicFactory) return E_FAIL; - return m_wicFactory->CreateBitmapFromMemory(width, height, format, stride, size, data, ppBitmap); -} - -// Standard JPEG luminance quantization table (Q=50) -static const int std_luminance_qtable[64] = { - 16, 11, 10, 16, 24, 40, 51, 61, - 12, 12, 14, 19, 26, 58, 60, 55, - 14, 13, 16, 24, 40, 57, 69, 56, - 14, 17, 22, 29, 51, 87, 80, 62, - 18, 22, 37, 56, 68, 109, 103, 77, - 24, 35, 55, 64, 81, 104, 113, 92, - 49, 64, 78, 87, 103, 121, 120, 101, - 72, 92, 95, 98, 112, 100, 103, 99 -}; - -// Estimate JPEG quality from quantization table in buffer -// Returns 0-100, or -1 if unable to parse -static int GetJpegQualityFromBuffer(const uint8_t* data, size_t size) { - if (!data || size < 20) return -1; - - // Search for DQT marker (0xFF, 0xDB) - for (size_t i = 0; i < size - 70; i++) { - if (data[i] == 0xFF && data[i + 1] == 0xDB) { - // Found DQT marker - size_t offset = i + 4; // Skip marker + length - if (offset + 64 >= size) return -1; - - // Read first 64 quantization values - int qtable[64]; - for (int j = 0; j < 64; j++) { - qtable[j] = data[offset + j]; - } - - // Calculate average scale compared to standard table - double total_scale = 0.0; - for (int j = 0; j < 64; j++) { - if (std_luminance_qtable[j] > 0) { - total_scale += (double)qtable[j] / std_luminance_qtable[j]; - } - } - double avg_scale = total_scale / 64.0; - - // Convert scale to quality (0-100) - int quality; - if (avg_scale <= 0.0) quality = 100; - else if (avg_scale >= 1.0) { - quality = (int)(50.0 / avg_scale); - } else { - quality = (int)(100.0 - (avg_scale * 50.0)); - } - - if (quality > 100) quality = 100; - if (quality < 1) quality = 1; - - return quality; - } - } - return -1; -} - -// ---------------------------------------------------------------------------- -// JPEG (libjpeg-turbo) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadJPEG(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector jpegBuf; - if (!ReadFileToVector(filePath, jpegBuf)) return E_FAIL; - - // Initialize TurboJPEG (Decompressor) - tjhandle tjInstance = tj3Init(TJINIT_DECOMPRESS); - if (!tjInstance) return E_FAIL; - - HRESULT hr = E_FAIL; - - // Parse header (TurboJPEG v3 API) - if (tj3DecompressHeader(tjInstance, jpegBuf.data(), jpegBuf.size()) == 0) { - - // Get dimensions from handle - int width = tj3Get(tjInstance, TJPARAM_JPEGWIDTH); - int height = tj3Get(tjInstance, TJPARAM_JPEGHEIGHT); - int jpegSubsamp = tj3Get(tjInstance, TJPARAM_SUBSAMP); - int jpegColorspace = tj3Get(tjInstance, TJPARAM_COLORSPACE); - - if (width > 0 && height > 0) { - // Decompress to BGRX (compatible with PBGRA/BGRA) - // Stride must be 4-byte aligned (width * 4 is always 4-byte aligned) - int pixelFormat = TJPF_BGRX; - int stride = width * 4; - size_t bufSize = (size_t)stride * height; - - std::vector pixelBuf(bufSize); - - if (tj3Decompress8(tjInstance, jpegBuf.data(), jpegBuf.size(), pixelBuf.data(), stride, pixelFormat) == 0) { - // Create WIC Bitmap from pixels - hr = CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppPBGRA, stride, (UINT)bufSize, pixelBuf.data(), ppBitmap); - - // Extract format details with quality estimation - if (SUCCEEDED(hr)) { - std::wstring subsamp; - switch (jpegSubsamp) { - case TJSAMP_444: subsamp = L"4:4:4"; break; - case TJSAMP_422: subsamp = L"4:2:2"; break; - case TJSAMP_420: subsamp = L"4:2:0"; break; - case TJSAMP_GRAY: subsamp = L"Gray"; break; - case TJSAMP_440: subsamp = L"4:4:0"; break; - default: subsamp = L""; break; - } - - // Estimate quality from quantization table - int quality = GetJpegQualityFromBuffer(jpegBuf.data(), jpegBuf.size()); - if (quality > 0) { - wchar_t buf[64]; - swprintf_s(buf, L"%s Q~%d", subsamp.c_str(), quality); - g_lastFormatDetails = buf; - } else { - g_lastFormatDetails = subsamp; - } - } - } - } - } - - tj3Destroy(tjInstance); - return hr; -} - -// ---------------------------------------------------------------------------- -// Step 2: Optimized Thumbnail Loading -// ---------------------------------------------------------------------------- - -HRESULT CImageLoader::LoadThumbJPEGFromMemory(const uint8_t* pBuf, size_t size, int targetSize, ThumbData* pData) { - if (!pData || !pBuf || size == 0) return E_INVALIDARG; - - tjhandle tj = tj3Init(TJINIT_DECOMPRESS); - if (!tj) return E_FAIL; - - // Helper to auto-destroy handle - struct TjGuard { tjhandle h; ~TjGuard() { if(h) tj3Destroy(h); } } guard{tj}; - - int width = 0, height = 0; - if (tj3DecompressHeader(tj, pBuf, size) < 0) { - return E_FAIL; - } - width = tj3Get(tj, TJPARAM_JPEGWIDTH); - height = tj3Get(tj, TJPARAM_JPEGHEIGHT); - pData->origWidth = width; - pData->origHeight = height; - pData->fileSize = size; - - if (width <= 0 || height <= 0) return E_FAIL; - - // Calculate Scaling Factor - int numFactors; - tjscalingfactor* factors = tj3GetScalingFactors(&numFactors); - tjscalingfactor chosenFactor = { 1, 1 }; - - // Find smallest factor that produces a dimension >= targetSize - int bestMetric = 999999; - for (int i = 0; i < numFactors; i++) { - int sW = TJSCALED(width, factors[i]); - int sH = TJSCALED(height, factors[i]); - // Ideally we want >= targetSize. - // If all are smaller, pick largest (1/1). - if (sW >= targetSize && sH >= targetSize) { - int metric = sW; - if (metric < bestMetric) { - bestMetric = metric; - chosenFactor = factors[i]; - } - } - } - - if (tj3SetScalingFactor(tj, chosenFactor) < 0) { - // Fallback to 1/1 - tjscalingfactor one = {1, 1}; - tj3SetScalingFactor(tj, one); - } - - int finalW = TJSCALED(width, chosenFactor); - int finalH = TJSCALED(height, chosenFactor); - - // Decode directly to target buffer - pData->width = finalW; - pData->height = finalH; - pData->stride = finalW * 4; - pData->pixels.resize(pData->stride * finalH); - - // Use TJPF_BGRA for D2D compatibility - // Note: TurboJPEG BGRA is straight alpha. If extracted thumb has no alpha (JPEG doesn't), it's opaque (A=255). - if (tj3Decompress8(tj, pBuf, size, pData->pixels.data(), pData->stride, TJPF_BGRA) < 0) { - return E_FAIL; - } - - pData->isValid = true; - return S_OK; -} - -HRESULT CImageLoader::LoadThumbWebPFromMemory(const uint8_t* pBuf, size_t size, int targetSize, ThumbData* pData) { - if (!pData || !pBuf || size == 0) return E_INVALIDARG; - - // Advanced API for threading + scaling support - WebPDecoderConfig config; - if (!WebPInitDecoderConfig(&config)) return E_FAIL; - - if (WebPGetFeatures(pBuf, size, &config.input) != VP8_STATUS_OK) return E_FAIL; - - int w = config.input.width; - int h = config.input.height; - pData->origWidth = w; - pData->origHeight = h; - pData->fileSize = size; - if (w <= 0 || h <= 0) return E_FAIL; - - // Calculate Ratio aiming for targetSize in smallest dimension (cover)? - // Or fit? - // TurboJPEG logic above tries to find a metric >= targetSize. - // Let's match typical scaler logic: Match one dimension to target size? - // Actually WebP scaling is arbitrary (unlike JPEG which is 1/2, 1/4, 1/8). - // So we can request exact size? - // WebP documentation says "scaled_width/height". - // "The output buffer will be scaled to these dimensions". - // So we can request exactly targetSize X something. - - // Let's compute exact target size maintaining aspect ratio. - float ratio = std::max((float)targetSize/w, (float)targetSize/h); - // If ratio >= 1.0 (Image smaller than target), keep original. - int newW, newH; - if (ratio >= 1.0f) { - newW = w; - newH = h; - } else { - newW = (int)(w * ratio); - newH = (int)(h * ratio); - if (newW < 1) newW = 1; - if (newH < 1) newH = 1; - } - - config.options.use_threads = 1; - config.options.use_scaling = 1; - config.options.scaled_width = newW; - config.options.scaled_height = newH; - config.options.no_fancy_upsampling = 1; // Speed up - config.output.colorspace = MODE_BGRA; // Direct BGRA - - // Decode - if (WebPDecode(pBuf, size, &config) != VP8_STATUS_OK) { - WebPFreeDecBuffer(&config.output); - return E_FAIL; - } - - // Copy/Move to ThumbData - // We could have decoded directly to pData->pixels if we resized it first and used external memory. - // But stride handling: WebP stride might differ? - // config.output.u.RGBA.stride usually is width*4 for MODE_BGRA. - - uint8_t* output = config.output.u.RGBA.rgba; - int stride = config.output.u.RGBA.stride; - int dataSize = config.output.u.RGBA.size; - - pData->width = newW; - pData->height = newH; - pData->stride = newW * 4; // Our target stride - - if (stride == pData->stride) { - pData->pixels.assign(output, output + dataSize); - } else { - // Copy row by row - pData->pixels.resize(pData->stride * newH); - for(int y=0; ypixels.data() + y * pData->stride; - memcpy(dst, src, std::min(stride, pData->stride)); - } - } - - pData->isValid = true; - WebPFreeDecBuffer(&config.output); - return S_OK; -} - -HRESULT CImageLoader::LoadThumbJPEG(LPCWSTR filePath, int targetSize, ThumbData* pData) { - if (!pData) return E_INVALIDARG; - - std::vector jpegBuf; - if (!ReadFileToVector(filePath, jpegBuf)) return E_FAIL; - - return LoadThumbJPEGFromMemory(jpegBuf.data(), jpegBuf.size(), targetSize, pData); -} - -HRESULT CImageLoader::LoadThumbnail(LPCWSTR filePath, int targetSize, ThumbData* pData) { - if (!pData) return E_INVALIDARG; - pData->isValid = false; - - // Get file size (cheap) - // Actually we iterate file path anyway. But if we read to vector, we know size. - // If we use WIC, we might not know size unless we query file. - // Let's use std::filesystem for non-vector paths logic - // But most paths read vector. - - // Detect format - std::wstring format = DetectFormatFromContent(filePath); - - // 1. Optimized Path (JPEG) - if (format == L"JPEG") { - if (SUCCEEDED(LoadThumbJPEG(filePath, targetSize, pData))) { - return S_OK; - } - } else if (format == L"WebP") { - // 1b. Optimized WebP (Scaled & Multithreaded) - std::vector buf; - if (ReadFileToVector(filePath, buf)) { - if (SUCCEEDED(LoadThumbWebPFromMemory(buf.data(), buf.size(), targetSize, pData))) { - return S_OK; - } - } - } - - // 1b. Optimized Extraction (RAW / HEIC / PSD) via PreviewExtractor - // Check extension logic or format details? - // DetectFormatFromContent usually returns "RAW (ARW)" or "HEIC". - // Let's use extension check for speed first (since Detect might read file). - // Actually LoadThumbnail passed filePath. - - std::wstring ext = filePath; - size_t dot = ext.find_last_of(L'.'); - if (dot != std::wstring::npos) { - std::wstring e = ext.substr(dot); - std::transform(e.begin(), e.end(), e.begin(), ::towlower); - - bool tryExtract = false; - bool isRaw = (e == L".arw" || e == L".cr2" || e == L".nef" || e == L".dng" || e == L".orf" || e == L".rw2" || e == L".raf"); - bool isHeic = (e == L".heic" || e == L".heif" || e == L".avif"); // AVIF might not have exif thumb, but worth try? - bool isPsd = (e == L".psd" || e == L".psb"); - - if (isRaw || isHeic || isPsd) { - // Read file header/content - // Note: RAW/PSD extraction might need significant portion of file. - // For now, read whole file. (Memory Mapped would be better, but Read works). - std::vector buf; - if (ReadFileToVector(filePath, buf)) { - PreviewExtractor::ExtractedData exData; - bool extracted = false; - - if (isRaw) extracted = PreviewExtractor::ExtractFromRAW(buf.data(), buf.size(), exData); - else if (isHeic) extracted = PreviewExtractor::ExtractFromHEIC(buf.data(), buf.size(), exData); - else if (isPsd) extracted = PreviewExtractor::ExtractFromPSD(buf.data(), buf.size(), exData); - - if (extracted && exData.IsValid()) { - // It's a JPEG buffer! - if (SUCCEEDED(LoadThumbJPEGFromMemory(exData.pData, exData.size, targetSize, pData))) { - // For extracted previews, we might not know the FULL RAW dimensions here unless parsed. - // However, preview dimensions (pData->origWidth) are set by LoadThumbJPEGFromMemory. - // That's the preview dimension, not RAW dimension. - // User might want RAW dimension. - // But getting RAW dimension requires parsing TIFF/HEIC headers fully. - // PreviewExtractor might have that info? - // For now, let's accept Preview Size or File Size. - // But File Size should be the RAW file size. - // buf.size() is the RAW file size. - pData->fileSize = buf.size(); - return S_OK; - } - } - } - } - } - - // 2. Fallback Path (WIC Scaler for everything else) - - // Fail Fast: Check Dimensions to prevent OOM on massive files (e.g. 20k x 20k) - // Only needed for fallback path which likely does full decode. - { - ComPtr decoder; - // Used GENERIC_READ. WIC Decoder is fast (reads header only). - if (SUCCEEDED(m_wicFactory->CreateDecoderFromFilename(filePath, nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder))) { - ComPtr frame; - if (SUCCEEDED(decoder->GetFrame(0, &frame))) { - UINT w = 0, h = 0; - frame->GetSize(&w, &h); - // Limit: 16384x16384 (268 MP). - // RGBA = 268 * 4 = 1GB per image. - // 1GB is too much for a thumbnail thread. - if (w > 16384 || h > 16384) { - return E_OUTOFMEMORY; - } - } - } - } - - ComPtr source; - // Use LoadFromFile to leverage specialized loaders where possible (e.g. Wuffs for PNG) - HRESULT hr = LoadFromFile(filePath, &source); - - if (FAILED(hr) || !source) return hr; - - // Scale - UINT origW, origH; - source->GetSize(&origW, &origH); - if (origW == 0 || origH == 0) return E_FAIL; - - // Compute ratio to fill targetSize (Center Crop style needs slightly larger coverage? - // Or just fit? Gallery uses Center Crop. So we should scale such that MIN(w,h) >= targetSize. - // Let's ensure both dims >= targetSize if possible, or at least one matches? - // Actually for center crop, we need the *smaller* dimension to be at least targetSize. - // wait, if we request targetSize, we usually mean the cell size. - // let's aim for the image covering targetSize x targetSize. - // So scale factor = max(targetSize/w, targetSize/h). - - double ratio = std::max((double)targetSize / origW, (double)targetSize / origH); - - UINT newW, newH; - if (ratio >= 1.0) { - // Image is smaller than thumbnail slot? Keep original (don't upscale bloat) - // Or upscale if needed? Let's just keep original effectively. - // Actually WIC Scaler handles upscaling too. - // For thumbnails, usually we are downscaling. - // If image is tiny, maybe just use it. - newW = origW; - newH = origH; - } else { - newW = (UINT)(origW * ratio); - newH = (UINT)(origH * ratio); - if (newW < 1) newW = 1; - if (newH < 1) newH = 1; - } - - // WIC Scaler - ComPtr scaler; - if (FAILED(m_wicFactory->CreateBitmapScaler(&scaler))) return E_FAIL; - - if (FAILED(scaler->Initialize(source.Get(), newW, newH, WICBitmapInterpolationModeFant))) return E_FAIL; - - // Format Converter (Ensure PBGRA/BGRA for D2D) - ComPtr converter; - if (FAILED(m_wicFactory->CreateFormatConverter(&converter))) return E_FAIL; - - // D2D prefers PBGRA usually, but we used BGRA for TurboJPEG. - // CreateBitmap functions usually accept both if specified correctly. - // Let's use PBGRA (Premultiplied BGRA) which works best with D2D alpha blending. - // TurboJPEG TJPF_BGRA is technically straight alpha (not premultiplied). - // D2D supports IgnoreAlpha or StraightAlpha via different flags, but PBGRA is standard. - // If JPG has no alpha, BGRA == PBGRA. So it's fine. - // But PNG might have alpha. WIC converter to PBGRA handles premultiplication. - - if (FAILED(converter->Initialize(scaler.Get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.f, WICBitmapPaletteTypeMedianCut))) return E_FAIL; - - // Copy to raw buffer - pData->width = newW; - pData->height = newH; - pData->stride = newW * 4; - pData->pixels.resize(pData->stride * newH); - - hr = converter->CopyPixels(nullptr, pData->stride, (UINT)pData->pixels.size(), pData->pixels.data()); - hr = converter->CopyPixels(nullptr, pData->stride, (UINT)pData->pixels.size(), pData->pixels.data()); - if (SUCCEEDED(hr)) { - pData->origWidth = origW; - pData->origHeight = origH; - // FileSize? - // Check file manually - WIN32_FILE_ATTRIBUTE_DATA fad; - if (GetFileAttributesEx(filePath, GetFileExInfoStandard, &fad)) { - pData->fileSize = (static_cast(fad.nFileSizeHigh) << 32) | fad.nFileSizeLow; - } - pData->isValid = true; - } - return hr; -} - -// LoadPNG REMOVED - replaced by LoadPngWuffs (Wuffs decoder) - -// ---------------------------------------------------------------------------- -// WebP (libwebp) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadWebP(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector webpBuf; - if (!ReadFileToVector(filePath, webpBuf)) return E_FAIL; - - // Advanced API for threading support - WebPDecoderConfig config; - if (!WebPInitDecoderConfig(&config)) return E_FAIL; - - // Enable multi-threaded decoding - config.options.use_threads = 1; - // Set output colorspace to BGRA (WIC compatible) - config.output.colorspace = MODE_BGRA; - - // Decode directly to buffer managed by WebP? - // No, standard flow is: - // 1. GetFeatures (to determine size) - // 2. Allocate buffer (optional, or let WebP do it) - // 3. Decode - - if (WebPGetFeatures(webpBuf.data(), webpBuf.size(), &config.input) != VP8_STATUS_OK) return E_FAIL; - - // Check dimensions - int width = config.input.width; - int height = config.input.height; - if (width == 0 || height == 0) return E_FAIL; - - // Decode - if (WebPDecode(webpBuf.data(), webpBuf.size(), &config) != VP8_STATUS_OK) { - WebPFreeDecBuffer(&config.output); - return E_FAIL; - } - - uint8_t* output = config.output.u.RGBA.rgba; - int stride = config.output.u.RGBA.stride; - int size = stride * height; - - HRESULT hr = CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppPBGRA, stride, size, output, ppBitmap); - - // Extract format details (VP8 = Lossy, VP8L/Lossless Flag = Lossless) - if (SUCCEEDED(hr)) { - // Check if lossless format (VP8L) - // WebP format: RIFF header + VP8/VP8L chunk - // VP8L signature: 0x2F at offset 15 (after RIFF+WEBP headers) - if (config.input.format == 2) { // VP8L = lossless - g_lastFormatDetails = L"Lossless"; - } else { - g_lastFormatDetails = L"Lossy"; - } - if (config.input.has_alpha) g_lastFormatDetails += L" +Alpha"; - } - - WebPFreeDecBuffer(&config.output); - return hr; -} - -// ---------------------------------------------------------------------------- -// AVIF (libavif + dav1d) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadAVIF(LPCWSTR filePath, IWICBitmap** ppBitmap) { - if (!filePath || !ppBitmap) return E_INVALIDARG; - - // Read file to memory buffer - std::vector avifBuf; - if (!ReadFileToVector(filePath, avifBuf)) return E_FAIL; - - // Create Decoder - avifDecoder* decoder = avifDecoderCreate(); - if (!decoder) return E_OUTOFMEMORY; - - // Enable multi-threaded decoding - // Use hardware_concurrency() to maximize throughput on multi-core CPUs - unsigned int threads = std::thread::hardware_concurrency(); - if (threads > 0) { - decoder->maxThreads = threads; - } else { - decoder->maxThreads = 4; // Fallback sensible default - } - - // Set Memory Source - avifResult result = avifDecoderSetIOMemory(decoder, avifBuf.data(), avifBuf.size()); - if (result != AVIF_RESULT_OK) { - avifDecoderDestroy(decoder); - return E_FAIL; - } - - // Parse - result = avifDecoderParse(decoder); - if (result != AVIF_RESULT_OK) { - avifDecoderDestroy(decoder); - return E_FAIL; - } - - // Next Image (Frame 0) - result = avifDecoderNextImage(decoder); - if (result != AVIF_RESULT_OK) { - avifDecoderDestroy(decoder); - return E_FAIL; - } - - // Convert YUV to RGB - avifRGBImage rgb; - avifRGBImageSetDefaults(&rgb, decoder->image); - - // Configure for WIC (BGRA, 8-bit) - rgb.format = AVIF_RGB_FORMAT_BGRA; - rgb.depth = 8; - - // Calculate stride and size - // Note: libavif might want to allocate its own pixels or proper stride - // Let's allocate our own buffer for safety and control - rgb.rowBytes = rgb.width * 4; - std::vector pixelData(rgb.rowBytes * rgb.height); - rgb.pixels = pixelData.data(); - - result = avifImageYUVToRGB(decoder->image, &rgb); - if (result != AVIF_RESULT_OK) { - avifDecoderDestroy(decoder); - return E_FAIL; - } - - // Create WIC Bitmap - HRESULT hr = CreateWICBitmapFromMemory(rgb.width, rgb.height, GUID_WICPixelFormat32bppPBGRA, (UINT)rgb.rowBytes, (UINT)pixelData.size(), pixelData.data(), ppBitmap); - - // Extract format details (bit depth) - if (SUCCEEDED(hr)) { - int depth = decoder->image->depth; - wchar_t buf[32]; - swprintf_s(buf, L"%d-bit", depth); - g_lastFormatDetails = buf; - if (decoder->image->alphaPlane != nullptr) g_lastFormatDetails += L" +Alpha"; - } - - avifDecoderDestroy(decoder); - return hr; -} - -// ---------------------------------------------------------------------------- -// JPEG XL (libjxl) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadJXL(LPCWSTR filePath, IWICBitmap** ppBitmap) { - if (!filePath || !ppBitmap) return E_INVALIDARG; - - std::vector jxlBuf; - if (!ReadFileToVector(filePath, jxlBuf)) return E_FAIL; - - // 1. Create Decoder and Runner - JxlDecoder* dec = JxlDecoderCreate(NULL); - if (!dec) return E_OUTOFMEMORY; - - // Use max threads (default to system CPU count) - void* runner = JxlResizableParallelRunnerCreate(NULL); - if (!runner) { - JxlDecoderDestroy(dec); - return E_OUTOFMEMORY; - } - - JxlDecoderSetParallelRunner(dec, JxlResizableParallelRunner, runner); - - // 2. Subscribe to events - if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)) { - JxlResizableParallelRunnerDestroy(runner); - JxlDecoderDestroy(dec); - return E_FAIL; - } - - // 3. Set Input - JxlDecoderSetInput(dec, jxlBuf.data(), jxlBuf.size()); - - JxlBasicInfo info; - JxlPixelFormat format = { 4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0 }; // RGBA - - std::vector pixels; - HRESULT hr = E_FAIL; - - // 4. Decode Loop - for (;;) { - JxlDecoderStatus status = JxlDecoderProcessInput(dec); - - if (status == JXL_DEC_ERROR) { - hr = E_FAIL; - break; - } - else if (status == JXL_DEC_SUCCESS) { - hr = S_OK; - break; - } - else if (status == JXL_DEC_BASIC_INFO) { - if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &info)) { - hr = E_FAIL; break; - } - // Resize buffer - size_t stride = info.xsize * 4; - pixels.resize(stride * info.ysize); - } - else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { - size_t bufferSize = pixels.size(); - JxlDecoderSetImageOutBuffer(dec, &format, pixels.data(), bufferSize); - } - else if (status == JXL_DEC_FULL_IMAGE) { - // Nothing to do, just continue - } - else { - // Unknown status or need more input (should not happen with full buffer) - // break; (Don't break, loop might need to continue) - } - } - - if (SUCCEEDED(hr)) { - // JXL outputs RGBA (by default assumption with 4 channels and standard). - // WIC needs a GUID. We use GUID_WICPixelFormat32bppRGBA. - // QuickView's RenderEngine might expect PBGRA. WIC FormatConverter usually runs after loading if needed (fallback uses it). - // BUT, CImageLoader::LoadToMemory returns a Bitmap. Main loop expects to Draw it. - // D2D CreateBitmapFromWicBitmap automatically converts if possible. - // So RGBA is fine. - hr = CreateWICBitmapFromMemory(info.xsize, info.ysize, GUID_WICPixelFormat32bppRGBA, info.xsize * 4, (UINT)pixels.size(), pixels.data(), ppBitmap); - } - - JxlResizableParallelRunnerDestroy(runner); - JxlDecoderDestroy(dec); - return hr; -} - -// ---------------------------------------------------------------------------- -// RAW (LibRaw) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadRaw(LPCWSTR filePath, IWICBitmap** ppBitmap, bool forceFullDecode) { - // Optimization: Try to load embedded JPEG preview first (FAST) - // Fallback: Full RAW decode (SLOW) - - std::vector rawBuf; - if (!ReadFileToVector(filePath, rawBuf)) return E_FAIL; - - LibRaw RawProcessor; - if (RawProcessor.open_buffer(rawBuf.data(), rawBuf.size()) != LIBRAW_SUCCESS) return E_FAIL; - - // 1. Try Unpack Thumbnail (Embedded Preview) - FASTEST - if (!forceFullDecode && RawProcessor.unpack_thumb() == LIBRAW_SUCCESS) { - int err = 0; - libraw_processed_image_t* thumb = RawProcessor.dcraw_make_mem_thumb(&err); - - if (thumb) { - if (thumb->type == LIBRAW_IMAGE_JPEG) { - // JPEG Thumbnail - ComPtr stream; - HRESULT hr = m_wicFactory->CreateStream(&stream); - if (SUCCEEDED(hr)) hr = stream->InitializeFromMemory(thumb->data, thumb->data_size); - - ComPtr decoder; - if (SUCCEEDED(hr)) hr = m_wicFactory->CreateDecoderFromStream(stream.Get(), nullptr, WICDecodeMetadataCacheOnDemand, &decoder); - - ComPtr frame; - if (SUCCEEDED(hr)) hr = decoder->GetFrame(0, &frame); - - ComPtr converter; - if (SUCCEEDED(hr)) hr = m_wicFactory->CreateFormatConverter(&converter); - if (SUCCEEDED(hr)) hr = converter->Initialize(frame.Get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.f, WICBitmapPaletteTypeMedianCut); - - if (SUCCEEDED(hr)) { - hr = m_wicFactory->CreateBitmapFromSource(converter.Get(), WICBitmapCacheOnLoad, ppBitmap); - } - - if (SUCCEEDED(hr)) { - RawProcessor.dcraw_clear_mem(thumb); - return hr; // Success with JPEG Preview! - } - } else if (thumb->type == LIBRAW_IMAGE_BITMAP) { - // Bitmap Thumbnail (RGB) - if (thumb->bits == 8 && thumb->colors == 3) { - UINT width = thumb->width; - UINT height = thumb->height; - UINT stride = width * 3; - HRESULT hr = CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat24bppRGB, stride, thumb->data_size, thumb->data, ppBitmap); - if (SUCCEEDED(hr)) { - RawProcessor.dcraw_clear_mem(thumb); - return hr; // Success with Bitmap Preview! - } - } - } - RawProcessor.dcraw_clear_mem(thumb); - } - } - - // 2. Fallback: Full Decode (Slow) - // Optimization: Disable Auto WB (slow), use Camera WB - RawProcessor.imgdata.params.use_camera_wb = 1; - RawProcessor.imgdata.params.use_auto_wb = 0; // Speed up - RawProcessor.imgdata.params.user_qual = 2; // 0=Linear(fast), 2=AHD(good), 3=AHD+Interpolation - - // If you want extreme speed at cost of resolution, uncomment: - // RawProcessor.imgdata.params.half_size = 1; - - if (RawProcessor.unpack() != LIBRAW_SUCCESS) return E_FAIL; - if (RawProcessor.dcraw_process() != LIBRAW_SUCCESS) return E_FAIL; - - libraw_processed_image_t* image = RawProcessor.dcraw_make_mem_image(); - if (!image) return E_FAIL; - - HRESULT hr = E_FAIL; - - if (image->type == LIBRAW_IMAGE_BITMAP) { - if (image->bits == 8 && image->colors == 3) { - UINT width = image->width; - UINT height = image->height; - UINT stride = width * 3; - hr = CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat24bppRGB, stride, image->data_size, image->data, ppBitmap); - } - } - - RawProcessor.dcraw_clear_mem(image); - return hr; -} - -HRESULT CImageLoader::LoadToMemory(LPCWSTR filePath, IWICBitmap** ppBitmap, std::wstring* pLoaderName, bool forceFullDecode) { - if (!filePath || !ppBitmap) return E_INVALIDARG; - - // Clear previous format details to avoid residue when switching formats - g_lastFormatDetails.clear(); - - std::wstring path = filePath; - std::transform(path.begin(), path.end(), path.begin(), ::towlower); - - // ------------------------------------------------------------- - // Architecture Upgrade: Robust Format Detection & Fallback - // ------------------------------------------------------------- - - // 1. Detect Format - std::wstring detectedFmt = DetectFormatFromContent(filePath); - - // ------------------------------------------------------------- - // Architecture Upgrade: Priority-Based Loading - // ------------------------------------------------------------- - - // 1. High-Performance Special Format Loaders (Bypass WIC) - if (detectedFmt == L"JPEG") { - HRESULT hr = LoadJPEG(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"libjpeg-turbo"; return S_OK; } - } - else if (detectedFmt == L"PNG") { - HRESULT hr = LoadPngWuffs(filePath, ppBitmap); // Wuffs is faster/safer - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs PNG"; return S_OK; } - } - else if (detectedFmt == L"WebP") { - HRESULT hr = LoadWebP(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"libwebp"; return S_OK; } - } - else if (detectedFmt == L"AVIF") { - HRESULT hr = LoadAVIF(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"libavif"; return S_OK; } - } - else if (detectedFmt == L"JXL") { - HRESULT hr = LoadJXL(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"libjxl"; return S_OK; } - } - else if (detectedFmt == L"GIF") { - HRESULT hr = LoadGifWuffs(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs GIF"; return S_OK; } - } - else if (detectedFmt == L"BMP") { - HRESULT hr = LoadBmpWuffs(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs BMP"; return S_OK; } - } - else if (detectedFmt == L"TGA") { - HRESULT hr = LoadTgaWuffs(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs TGA"; return S_OK; } - } - else if (detectedFmt == L"WBMP") { - HRESULT hr = LoadWbmpWuffs(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs WBMP"; return S_OK; } - } - else if (detectedFmt == L"PSD") { - HRESULT hr = LoadStbImage(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Stb Image (PSD)"; return S_OK; } - } - else if (detectedFmt == L"HDR") { - HRESULT hr = LoadStbImage(filePath, ppBitmap, true); // float - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Stb Image (HDR)"; return S_OK; } - } - else if (detectedFmt == L"PIC") { - HRESULT hr = LoadStbImage(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Stb Image (PIC)"; return S_OK; } - } - else if (detectedFmt == L"PNM") { - // Try Wuffs first (safer), fallback to Stb? Not needed, Wuffs covers well. - HRESULT hr = LoadNetpbmWuffs(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs NetPBM"; return S_OK; } - } - else if (detectedFmt == L"EXR") { - HRESULT hr = LoadTinyExrImage(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"TinyEXR"; return S_OK; } - } - else if (detectedFmt == L"SVG") { - HRESULT hr = LoadSVG(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"NanoSVG"; return S_OK; } - } - else if (detectedFmt == L"QOI") { - HRESULT hr = LoadQoiWuffs(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Wuffs QOI"; return S_OK; } - } - else if (detectedFmt == L"PCX") { - HRESULT hr = LoadPCX(filePath, ppBitmap); - if (SUCCEEDED(hr)) { if (pLoaderName) *pLoaderName = L"Custom PCX"; return S_OK; } - } - - // RAW Check (no magic bytes usually reliable, use extension + try) - if (detectedFmt == L"Unknown") { // Raw files often have no standard magic or complicated ones - // Check extension - if (path.ends_with(L".arw") || path.ends_with(L".cr2") || path.ends_with(L".nef") || - path.ends_with(L".dng") || path.ends_with(L".orf") || path.ends_with(L".rw2") || - path.ends_with(L".raf") || path.ends_with(L".pef") || path.ends_with(L".srw") || path.ends_with(L".cr3")) { - HRESULT hr = LoadRaw(filePath, ppBitmap, forceFullDecode); - if (SUCCEEDED(hr)) { - if (pLoaderName) *pLoaderName = forceFullDecode ? L"LibRaw (Full)" : L"LibRaw (Preview)"; - return S_OK; - } - } - } - - - // 3. Robust Fallback to WIC (Standard Loading) - if (pLoaderName) *pLoaderName = L"WIC (Fallback)"; - - // If High-Perf loader failed (e.g. malformed specific header, unsupported feature) OR format verified but unimplemented (stub), - // OR format unknown. - // --------------------------------------------------------- - - // 1. Load Lazy Source - ComPtr source; - // Note: Can't use this->LoadFromFile nicely if we want to avoid double-open, - // but LoadFromFile uses WIC factory directly. Let's just use the WIC path inline or call existing helper. - // Re-use existing WIC fallback logic: - - ComPtr decoder; - HRESULT hr = m_wicFactory->CreateDecoderFromFilename( - filePath, nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder - ); - if (FAILED(hr)) return hr; - - ComPtr frame; - hr = decoder->GetFrame(0, &frame); - if (FAILED(hr)) return hr; - - // 2. Convert to D2D Compatible Format (PBGRA32) - ComPtr converter; - hr = m_wicFactory->CreateFormatConverter(&converter); - if (FAILED(hr)) return hr; - - hr = converter->Initialize( - frame.Get(), // Use frame source - GUID_WICPixelFormat32bppPBGRA, - WICBitmapDitherTypeNone, - nullptr, - 0.f, - WICBitmapPaletteTypeMedianCut - ); - if (FAILED(hr)) return hr; - - // 3. Force Decode to Memory - return m_wicFactory->CreateBitmapFromSource( - converter.Get(), - WICBitmapCacheOnLoad, - ppBitmap - ); -} - -std::wstring CImageLoader::GetLastFormatDetails() const { - return g_lastFormatDetails; -} - -HRESULT CImageLoader::GetImageSize(LPCWSTR filePath, UINT* width, UINT* height) { - if (!filePath || !width || !height) return E_INVALIDARG; - - ComPtr bitmap; - HRESULT hr = LoadFromFile(filePath, &bitmap); - if (FAILED(hr)) return hr; - - return bitmap->GetSize(width, height); -} - -// ---------------------------------------------------------------------------- -// Wuffs PNG Decoder (Google's memory-safe decoder) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadPngWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector pngBuf; - if (!ReadFileToVector(filePath, pngBuf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodePNG(pngBuf.data(), pngBuf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)pixelData.size(), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Wuffs GIF Decoder (First frame only for now) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadGifWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector gifBuf; - if (!ReadFileToVector(filePath, gifBuf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodeGIF(gifBuf.data(), gifBuf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)pixelData.size(), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Wuffs BMP Decoder -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadBmpWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodeBMP(buf.data(), buf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)pixelData.size(), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Wuffs TGA Decoder -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadTgaWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodeTGA(buf.data(), buf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)pixelData.size(), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Wuffs WBMP Decoder -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadWbmpWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodeWBMP(buf.data(), buf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)pixelData.size(), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Stb Image Decoder (PSD, HDR, PIC, PNM) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadStbImage(LPCWSTR filePath, IWICBitmap** ppBitmap, bool floatFormat) { - // Read file to memory first (Solves Windows Unicode Path issues reliably) - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - int width = 0, height = 0, channels = 0; - std::vector pixelData; - - // Use Memory Loader - if (!StbLoader::LoadImageFromMemory(buf.data(), buf.size(), &width, &height, &channels, pixelData, floatFormat)) { - return E_FAIL; - } - - if (floatFormat) { - // HDR: Create float bitmap (128bpp RGBA Float) - size_t stride = width * 4 * sizeof(float); - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat128bppRGBAFloat, - (UINT)stride, (UINT)pixelData.size(), (BYTE*)pixelData.data(), ppBitmap); - } else { - // Standard 8-bit RGBA - // WIC prefers BGRA for 32bpp - // Swap R and B - uint8_t* p = pixelData.data(); - size_t pixelCount = (size_t)width * height; - for (size_t i = 0; i < pixelCount; i++) { - std::swap(p[i*4], p[i*4+2]); // Swap R and B - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)pixelData.size(), pixelData.data(), ppBitmap); - } -} - -// ---------------------------------------------------------------------------- -// TinyEXR Decoder -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadTinyExrImage(LPCWSTR filePath, IWICBitmap** ppBitmap) { - // Read file to memory (Solves Path and Locking issues) - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - int width = 0, height = 0; - std::vector pixelData; - - // Use Memory Loader - if (!TinyExrLoader::LoadEXRFromMemory(buf.data(), buf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - // TinyEXR returns RGBA floats. - // Create WIC Bitmap (128bpp RGBA Float) - size_t stride = width * 4 * sizeof(float); - // Note: pixelData.size() is count of floats. Size in bytes is size() * sizeof(float). - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat128bppRGBAFloat, - (UINT)stride, (UINT)(pixelData.size() * sizeof(float)), (BYTE*)pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// NanoSVG Decoder (SVG Support) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadSVG(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector fileData; - if (!ReadFileToVector(filePath, fileData)) return E_FAIL; - - // NanoSVG parses char* string (null terminated recommended) - std::vector xmlData(fileData.begin(), fileData.end()); - xmlData.push_back('\0'); - - // Parse (96 DPI default units) - // Note: nsvgParse modifies the input string content during parsing (destructive) - NSVGimage* image = nsvgParse(xmlData.data(), "px", 96.0f); - if (!image) return E_FAIL; - - // Scale Logic: 2.0x for crisp rendering - float scale = 2.0f; - - // Safety size limit (e.g. 8k) - float maxDim = 8192.0f; - if (image->width * scale > maxDim || image->height * scale > maxDim) { - float aspect = image->width / image->height; - if (aspect > 1.0f) { - scale = maxDim / image->width; - } else { - scale = maxDim / image->height; - } - } - - int width = (int)(image->width * scale); - int height = (int)(image->height * scale); - - if (width <= 0 || height <= 0) { - nsvgDelete(image); - return E_FAIL; - } - - // Rasterize - NSVGrasterizer* rast = nsvgCreateRasterizer(); - if (!rast) { - nsvgDelete(image); - return E_OUTOFMEMORY; - } - - // NanoSVG generates RGBA (32-bit) - size_t stride = width * 4; - size_t size = stride * height; - std::vector imgData(size); - - nsvgRasterize(rast, image, 0, 0, scale, imgData.data(), width, height, (int)stride); - - nsvgDeleteRasterizer(rast); - nsvgDelete(image); - - // Create WIC Bitmap (GUID_WICPixelFormat32bppRGBA) - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppRGBA, - (UINT)stride, (UINT)size, imgData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Wuffs NetPBM (PAM, PBM, PGM, PPM) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadNetpbmWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodeNetpbm(buf.data(), buf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)(stride * height), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Wuffs QOI (Quite OK Image) -// ---------------------------------------------------------------------------- -HRESULT CImageLoader::LoadQoiWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector buf; - if (!ReadFileToVector(filePath, buf)) return E_FAIL; - - uint32_t width = 0, height = 0; - std::vector pixelData; - - if (!WuffsLoader::DecodeQOI(buf.data(), buf.size(), &width, &height, pixelData)) { - return E_FAIL; - } - - size_t stride = width * 4; - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppBGRA, - (UINT)stride, (UINT)(stride * height), pixelData.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Custom PCX Decoder (Since stb_image lacks support) -// ---------------------------------------------------------------------------- -#pragma pack(push, 1) -struct PCXHeader { - uint8_t manufacturer; // 0x0A - uint8_t version; - uint8_t encoding; // 1 = RLE - uint8_t bitsPerPixel; - uint16_t xmin, ymin, xmax, ymax; - uint16_t hDpi, vDpi; - uint8_t palette[48]; - uint8_t reserved; - uint8_t colorPlanes; - uint16_t bytesPerLine; - uint16_t paletteType; - uint16_t hScreenSize, vScreenSize; - uint8_t filler[54]; -}; -#pragma pack(pop) - -HRESULT CImageLoader::LoadPCX(LPCWSTR filePath, IWICBitmap** ppBitmap) { - std::vector data; - if (!ReadFileToVector(filePath, data)) return E_FAIL; - - if (data.size() < sizeof(PCXHeader)) return E_FAIL; - - const PCXHeader* header = (const PCXHeader*)data.data(); - if (header->manufacturer != 0x0A) return E_FAIL; - if (header->encoding != 1) return E_FAIL; // Only RLE supported - - int width = header->xmax - header->xmin + 1; - int height = header->ymax - header->ymin + 1; - if (width <= 0 || height <= 0) return E_FAIL; - if (width > 32768 || height > 32768) return E_FAIL; - - // Decode RLE - size_t scanlineBytes = header->bytesPerLine * header->colorPlanes; - std::vector decodedBytes(scanlineBytes * height); - uint8_t* dst = decodedBytes.data(); - uint8_t* dstEnd = dst + decodedBytes.size(); - - const uint8_t* ptr = data.data() + 128; - const uint8_t* end = data.data() + data.size(); - - while (dst < dstEnd && ptr < end) { - uint8_t byte = *ptr++; - if ((byte & 0xC0) == 0xC0) { - uint8_t count = byte & 0x3F; - if (ptr >= end) break; - uint8_t val = *ptr++; - if (dst + count > dstEnd) count = (uint8_t)(dstEnd - dst); - memset(dst, val, count); - dst += count; - } else { - *dst++ = byte; - } - } - - // Convert to RGBA - std::vector rgba(width * height * 4); - - if (header->colorPlanes == 3 && header->bitsPerPixel == 8) { - // 24-bit RGB - for (int y = 0; y < height; y++) { - uint8_t* rowSrc = decodedBytes.data() + y * scanlineBytes; - uint8_t* rSrc = rowSrc; - uint8_t* gSrc = rowSrc + header->bytesPerLine; - uint8_t* bSrc = rowSrc + header->bytesPerLine * 2; - uint8_t* dstRow = rgba.data() + y * width * 4; - - for (int x = 0; x < width; x++) { - if (x < header->bytesPerLine) { - dstRow[x * 4 + 0] = rSrc[x]; - dstRow[x * 4 + 1] = gSrc[x]; - dstRow[x * 4 + 2] = bSrc[x]; - dstRow[x * 4 + 3] = 255; - } - } - } - } else if (header->colorPlanes == 1 && header->bitsPerPixel == 8) { - // 256 color palette (at end of file) - if (data.size() < 769) return E_FAIL; - const uint8_t* palPtr = data.data() + data.size() - 768; - - for (int y = 0; y < height; y++) { - uint8_t* rowSrc = decodedBytes.data() + y * scanlineBytes; - uint8_t* dstRow = rgba.data() + y * width * 4; - for (int x = 0; x < width; x++) { - if (x < header->bytesPerLine) { - uint8_t idx = rowSrc[x]; - dstRow[x * 4 + 0] = palPtr[idx * 3 + 0]; - dstRow[x * 4 + 1] = palPtr[idx * 3 + 1]; - dstRow[x * 4 + 2] = palPtr[idx * 3 + 2]; - dstRow[x * 4 + 3] = 255; - } - } - } - } else { - return E_FAIL; // Unsupported format - } - - return CreateWICBitmapFromMemory(width, height, GUID_WICPixelFormat32bppRGBA, - width * 4, width * height * 4, rgba.data(), ppBitmap); -} - -// ---------------------------------------------------------------------------- -// Metadata Implementation -// ---------------------------------------------------------------------------- - -#include -#include -#include -#pragma comment(lib, "propsys.lib") - -static HRESULT GetMetadataString(IWICMetadataQueryReader* reader, LPCWSTR query, std::wstring& out) { - if (!reader) return E_INVALIDARG; - PROPVARIANT val; PropVariantInit(&val); - HRESULT hr = reader->GetMetadataByName(query, &val); - if (SUCCEEDED(hr)) { - WCHAR buf[512] = {}; - if (SUCCEEDED(PropVariantToString(val, buf, 512))) { - out = buf; - } - PropVariantClear(&val); - } - return hr; -} - -static double DecodeRational(unsigned __int64 val) { - uint32_t num = (uint32_t)(val & 0xFFFFFFFF); - uint32_t den = (uint32_t)(val >> 32); - if (den == 0) return 0.0; - return (double)num / (double)den; -} - -static double DecodeSignedRational(unsigned __int64 val) { - // EXIF SRATIONAL: Low 32 bits = signed numerator, High 32 bits = signed denominator - int32_t num = (int32_t)(val & 0xFFFFFFFF); - int32_t den = (int32_t)(val >> 32); - if (den == 0) return 0.0; - return (double)num / (double)den; -} - -static HRESULT GetMetadataSignedRational(IWICMetadataQueryReader* reader, LPCWSTR query, double* out) { - if (!reader || !out) return E_INVALIDARG; - PROPVARIANT val; PropVariantInit(&val); - HRESULT hr = reader->GetMetadataByName(query, &val); - if (SUCCEEDED(hr)) { - if (val.vt == VT_UI8) { - *out = DecodeSignedRational(val.uhVal.QuadPart); - } else if (val.vt == VT_I8) { - *out = DecodeSignedRational((unsigned __int64)val.hVal.QuadPart); - } else if (val.vt == VT_I4) { - *out = (double)val.lVal; // Already a ratio? - } else if (val.vt == VT_R8) { - *out = val.dblVal; - } else { - // Fallback: Try standard conversion but it may fail for RATIONAL - hr = E_FAIL; // Skip if unknown type - } - PropVariantClear(&val); - } - return hr; -} - -static HRESULT GetMetadataRational(IWICMetadataQueryReader* reader, LPCWSTR query, double* out) { - if (!reader || !out) return E_INVALIDARG; - PROPVARIANT val; PropVariantInit(&val); - HRESULT hr = reader->GetMetadataByName(query, &val); - if (SUCCEEDED(hr)) { - if (val.vt == VT_UI8) { - *out = DecodeRational(val.uhVal.QuadPart); - } else { - PropVariantToDouble(val, out); - } - PropVariantClear(&val); - } - return hr; -} - - - -static HRESULT GetMetadataGPS(IWICMetadataQueryReader* reader, LPCWSTR coordQuery, LPCWSTR refQuery, double* outVal) { - if (!reader || !outVal) return E_INVALIDARG; - - // 1. Read Reference (N/S/E/W) - WCHAR refBuf[16] = {}; - PROPVARIANT varRef; PropVariantInit(&varRef); - if (SUCCEEDED(reader->GetMetadataByName(refQuery, &varRef))) { - PropVariantToString(varRef, refBuf, 16); - PropVariantClear(&varRef); - } - - // 2. Read Coordinate (Vector of 3 UI8s) - PROPVARIANT varCoord; PropVariantInit(&varCoord); - HRESULT hr = reader->GetMetadataByName(coordQuery, &varCoord); - if (FAILED(hr)) return hr; - - double result = 0.0; - - if (varCoord.vt == (VT_UI8 | VT_VECTOR)) { - if (varCoord.cauh.cElems == 3) { - double deg = DecodeRational(varCoord.cauh.pElems[0].QuadPart); - double min = DecodeRational(varCoord.cauh.pElems[1].QuadPart); - double sec = DecodeRational(varCoord.cauh.pElems[2].QuadPart); - result = deg + min/60.0 + sec/3600.0; - } - } - PropVariantClear(&varCoord); - - // Apply Ref (S or W means negative) - if (refBuf[0] == 'S' || refBuf[0] == 's' || refBuf[0] == 'W' || refBuf[0] == 'w') { - result = -result; - } - - *outVal = result; - return S_OK; -} - -HRESULT CImageLoader::ReadMetadata(LPCWSTR filePath, ImageMetadata* pMetadata) { - if (!filePath || !pMetadata) return E_INVALIDARG; - *pMetadata = {}; // Clear - - if (!m_wicFactory) return E_FAIL; - - // 1. Detect Format (New robust logic) - pMetadata->Format = DetectFormatFromContent(filePath); - - // 2. Create Decoder based on file (Safe fallback) - ComPtr decoder; - HRESULT hr = m_wicFactory->CreateDecoderFromFilename(filePath, nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder); - - if (FAILED(hr)) { - // If WIC failed but we detected a format (e.g. JXL, Custom RAW), return S_OK so we at least have the Format field - if (pMetadata->Format != L"Unknown") return S_OK; - return hr; - } - - // WIC Format check (Secondary verification or detail) - // If our magic byte detector returned generic or WIC has better info? - // Actually our detector is better for JXL/AVIF/WebP often. - // Just keep the Magic Byte result as primary. - /* - GUID containerFormat; - if (SUCCEEDED(decoder->GetContainerFormat(&containerFormat))) { - // ... WIC detection fallback if needed ... - } - */ - - // 2. Get Frame 0 - ComPtr frame; - hr = decoder->GetFrame(0, &frame); - if (FAILED(hr)) return S_FALSE; - - // 3. Basic Info - frame->GetSize(&pMetadata->Width, &pMetadata->Height); - - // File Size & Time Fallback - HANDLE hFile = CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile != INVALID_HANDLE_VALUE) { - LARGE_INTEGER size; - if (GetFileSizeEx(hFile, &size)) pMetadata->FileSize = size.QuadPart; - - // Get File Modify Time (Fallback for Date) - FILETIME ftWrite; - if (GetFileTime(hFile, nullptr, nullptr, &ftWrite)) { - SYSTEMTIME st; FileTimeToSystemTime(&ftWrite, &st); - wchar_t buf[64]; - swprintf_s(buf, L"%04d-%02d-%02d %02d:%02d:%02d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); - // We store it, but if EXIF exists, we overwrite - pMetadata->Date = buf; - } - CloseHandle(hFile); - } - - // 4. Metadata Query - ComPtr reader; - if (FAILED(frame->GetMetadataQueryReader(&reader))) return S_OK; - - // 5. Read Tags - // 5. Read Tags (Try multiple paths for robustness) - const wchar_t* makeQueries[] = { L"/app1/ifd/{ushort=271}", L"/ifd/{ushort=271}" }; - for (auto q : makeQueries) if (SUCCEEDED(GetMetadataString(reader.Get(), q, pMetadata->Make)) && !pMetadata->Make.empty()) break; - - const wchar_t* modelQueries[] = { L"/app1/ifd/{ushort=272}", L"/ifd/{ushort=272}" }; - for (auto q : modelQueries) if (SUCCEEDED(GetMetadataString(reader.Get(), q, pMetadata->Model)) && !pMetadata->Model.empty()) break; - - const wchar_t* dateQueries[] = { L"/app1/ifd/exif/{ushort=36867}", L"/app1/ifd/{ushort=306}", L"/ifd/exif/{ushort=36867}", L"/ifd/{ushort=306}" }; - for (auto q : dateQueries) if (SUCCEEDED(GetMetadataString(reader.Get(), q, pMetadata->Date)) && !pMetadata->Date.empty()) break; - - // ISO (34855) - std::wstring iso; - const wchar_t* isoQueries[] = { L"/app1/ifd/exif/{ushort=34855}", L"/ifd/exif/{ushort=34855}", L"/app1/ifd/{ushort=34855}" }; - for (auto q : isoQueries) { - if (SUCCEEDED(GetMetadataString(reader.Get(), q, iso)) && !iso.empty()) { - pMetadata->ISO = iso; break; - } - } - - // Aperture (FNumber 33437) - double fnum = 0.0; - const wchar_t* fnumQueries[] = { L"/app1/ifd/exif/{ushort=33437}", L"/ifd/exif/{ushort=33437}", L"/app1/ifd/{ushort=33437}" }; - for (auto q : fnumQueries) { - if (SUCCEEDED(GetMetadataRational(reader.Get(), q, &fnum))) { - wchar_t buf[32]; swprintf_s(buf, L"f/%.1f", fnum); - pMetadata->Aperture = buf; - break; - } - } - - // ExposureTime (33434) - double expTime = 0.0; - const wchar_t* expQueries[] = { L"/app1/ifd/exif/{ushort=33434}", L"/ifd/exif/{ushort=33434}", L"/app1/ifd/{ushort=33434}" }; - for (auto q : expQueries) { - if (SUCCEEDED(GetMetadataRational(reader.Get(), q, &expTime))) { - if (expTime > 0) { - if (expTime < 1.0) { - wchar_t buf[32]; swprintf_s(buf, L"1/%.0f", 1.0 / expTime); - pMetadata->Shutter = buf; - } else { - wchar_t buf[32]; swprintf_s(buf, L"%.1f\"", expTime); - pMetadata->Shutter = buf; - } - } - break; - } - } - - // FocalLength (37386) - double focal = 0.0; - const wchar_t* focalQueries[] = { L"/app1/ifd/exif/{ushort=37386}", L"/ifd/exif/{ushort=37386}", L"/app1/ifd/{ushort=37386}" }; - for (auto q : focalQueries) { - if (SUCCEEDED(GetMetadataRational(reader.Get(), q, &focal))) { - wchar_t buf[64]; - // Try to get 35mm equivalent (0xa405) - double focal35 = 0.0; - const wchar_t* focal35Queries[] = { L"/app1/ifd/exif/{ushort=41989}", L"/ifd/exif/{ushort=41989}" }; - for (auto q35 : focal35Queries) { - if (SUCCEEDED(GetMetadataRational(reader.Get(), q35, &focal35)) && focal35 > 0) break; - } - if (focal35 > 0) { - swprintf_s(buf, L"%.0fmm (%.0fmm)", focal, focal35); - } else { - swprintf_s(buf, L"%.0fmm", focal); - } - pMetadata->Focal = buf; - break; - } - } - - // Lens Model (42036 or 0xA434) - const wchar_t* lensQueries[] = { L"/app1/ifd/exif/{ushort=42036}", L"/ifd/exif/{ushort=42036}" }; - for (auto q : lensQueries) if (SUCCEEDED(GetMetadataString(reader.Get(), q, pMetadata->Lens)) && !pMetadata->Lens.empty()) break; - - // Software (305) - Try multiple paths - const wchar_t* softQueries[] = { L"/app1/ifd/{ushort=305}", L"/ifd/{ushort=305}" }; - for (auto q : softQueries) if (SUCCEEDED(GetMetadataString(reader.Get(), q, pMetadata->Software)) && !pMetadata->Software.empty()) break; - - // ColorSpace Detection: - // 1. Try to get ICC Profile embedded in image for accurate color space name - // 2. Fall back to EXIF ColorSpace tag (0xa001) - - // Try ICC Profile first - Get color context from frame - ComPtr iccFrame; - if (SUCCEEDED(decoder->GetFrame(0, &iccFrame))) { - ComPtr colorContext; - if (SUCCEEDED(m_wicFactory->CreateColorContext(&colorContext))) { - UINT count = 0; - IWICColorContext* contexts[1] = { colorContext.Get() }; - hr = iccFrame->GetColorContexts(1, contexts, &count); - if (SUCCEEDED(hr) && count > 0) { - // Get profile bytes and parse description - UINT profileSize = 0; - colorContext->GetProfileBytes(0, nullptr, &profileSize); - if (profileSize > 128) { // ICC profile header is 128 bytes - std::vector profileData(profileSize); - if (SUCCEEDED(colorContext->GetProfileBytes(profileSize, profileData.data(), &profileSize))) { - // ICC Profile structure: - // Offset 12-15: Profile/Device class (signature) - // We need to find 'desc' tag for profile description - // Tag table starts at offset 128 - UINT tagCount = (profileData[128] << 24) | (profileData[129] << 16) | (profileData[130] << 8) | profileData[131]; - for (UINT i = 0; i < tagCount && i < 50; i++) { - UINT tagOffset = 132 + i * 12; - if (tagOffset + 12 > profileSize) break; - // Check for 'desc' tag signature - if (profileData[tagOffset] == 'd' && profileData[tagOffset+1] == 'e' && - profileData[tagOffset+2] == 's' && profileData[tagOffset+3] == 'c') { - UINT descOffset = (profileData[tagOffset+4] << 24) | (profileData[tagOffset+5] << 16) | - (profileData[tagOffset+6] << 8) | profileData[tagOffset+7]; - UINT descSize = (profileData[tagOffset+8] << 24) | (profileData[tagOffset+9] << 16) | - (profileData[tagOffset+10] << 8) | profileData[tagOffset+11]; - if (descOffset + 12 < profileSize && descSize > 12) { - // Skip type signature (4 bytes) and reserved (4 bytes) - // ASCII count at offset+8 - UINT strLen = (profileData[descOffset+8] << 24) | (profileData[descOffset+9] << 16) | - (profileData[descOffset+10] << 8) | profileData[descOffset+11]; - if (strLen > 0 && strLen < 128 && descOffset + 12 + strLen <= profileSize) { - std::string desc((char*)&profileData[descOffset + 12], strLen - 1); - // Common profiles - if (desc.find("Display P3") != std::string::npos) pMetadata->ColorSpace = L"Display P3"; - else if (desc.find("sRGB") != std::string::npos) pMetadata->ColorSpace = L"sRGB"; - else if (desc.find("Adobe RGB") != std::string::npos) pMetadata->ColorSpace = L"Adobe RGB"; - else if (desc.find("DCI-P3") != std::string::npos) pMetadata->ColorSpace = L"DCI-P3"; - else if (desc.find("Rec. 2020") != std::string::npos || desc.find("BT.2020") != std::string::npos) pMetadata->ColorSpace = L"Rec.2020"; - else { - // Use first 20 chars of description - std::wstring wdesc(desc.begin(), desc.end()); - if (wdesc.length() > 20) wdesc = wdesc.substr(0, 20) + L"..."; - pMetadata->ColorSpace = wdesc; - } - } - } - break; - } - } - } - } - } - } - } - - // Fallback to EXIF ColorSpace if ICC not found - if (pMetadata->ColorSpace.empty()) { - PROPVARIANT csVar; PropVariantInit(&csVar); - const wchar_t* csQueries[] = { L"/app1/ifd/exif/{ushort=40961}", L"/ifd/exif/{ushort=40961}" }; - for (auto q : csQueries) { - if (SUCCEEDED(reader->GetMetadataByName(q, &csVar))) { - UINT csVal = 0; - if (csVar.vt == VT_UI2) csVal = csVar.uiVal; - else if (csVar.vt == VT_UI4) csVal = csVar.ulVal; - - if (csVal == 1) pMetadata->ColorSpace = L"sRGB"; - else if (csVal == 2) pMetadata->ColorSpace = L"Adobe RGB"; - else if (csVal == 65535) pMetadata->ColorSpace = L"Wide Gamut"; - PropVariantClear(&csVar); - break; - } - } - } - - // Exposure Bias (37380) - Signed Rational - double bias = 0.0; - const wchar_t* biasQueries[] = { L"/app1/ifd/exif/{ushort=37380}", L"/ifd/exif/{ushort=37380}" }; - for (auto q : biasQueries) if (SUCCEEDED(GetMetadataSignedRational(reader.Get(), q, &bias))) { - if (std::abs(bias) > 0.0001) { // Check non-zero with epsilon - wchar_t buf[32]; swprintf_s(buf, L"%+.1f ev", bias); - pMetadata->ExposureBias = buf; - } - break; - } - - // Flash (37377) - PROPVARIANT flashVar; PropVariantInit(&flashVar); - const wchar_t* flashQueries[] = { L"/app1/ifd/exif/{ushort=37377}", L"/ifd/exif/{ushort=37377}" }; - for (auto q : flashQueries) { - if (SUCCEEDED(reader->GetMetadataByName(q, &flashVar))) { - UINT flashVal = 0; - if (flashVar.vt == VT_UI2) flashVal = flashVar.uiVal; - else if (flashVar.vt == VT_UI4) flashVal = flashVar.ulVal; - - pMetadata->Flash = (flashVal & 1) ? L"Flash: On" : L"Flash: Off"; // Simple logic - if ((flashVal & 1) == 0) pMetadata->Flash = L""; // Don't show if Off? Or show "Flash: Off"? User said "Whether there is flash". "Off" is info. - // But usually clean UI hides "Off". "Windows Photo" shows it. - // I'll show it if it was explicitly recorded. - PropVariantClear(&flashVar); - break; - } - } - - // GPS - double lat = 0, lon = 0; - bool foundGPS = false; - - // Block 1: Try /app1/ifd/gps/... - if (SUCCEEDED(GetMetadataGPS(reader.Get(), L"/app1/ifd/gps/{ushort=2}", L"/app1/ifd/gps/{ushort=1}", &lat)) && - SUCCEEDED(GetMetadataGPS(reader.Get(), L"/app1/ifd/gps/{ushort=4}", L"/app1/ifd/gps/{ushort=3}", &lon))) { - foundGPS = true; - } - // Block 2: Try /ifd/gps/... (Fallback) - else if (SUCCEEDED(GetMetadataGPS(reader.Get(), L"/ifd/gps/{ushort=2}", L"/ifd/gps/{ushort=1}", &lat)) && - SUCCEEDED(GetMetadataGPS(reader.Get(), L"/ifd/gps/{ushort=4}", L"/ifd/gps/{ushort=3}", &lon))) { - foundGPS = true; - } - - if (foundGPS) { - pMetadata->HasGPS = true; - pMetadata->Latitude = lat; - pMetadata->Longitude = lon; - - // Altitude - Try multiple paths for HEIC/JPEG compatibility - double alt = 0; - const wchar_t* altQueries[] = { - L"/app1/ifd/gps/{ushort=6}", - L"/ifd/gps/{ushort=6}", - L"/ifd/{ushort=34853}/{ushort=6}" // GPS IFD pointer method - }; - for (auto q : altQueries) { - if (SUCCEEDED(GetMetadataRational(reader.Get(), q, &alt))) { - pMetadata->Altitude = alt; - // Check Ref (5): 0=Above, 1=Below - PROPVARIANT altRef; PropVariantInit(&altRef); - // Try corresponding ref path - std::wstring refPath = q; - size_t pos = refPath.rfind(L"6}"); - if (pos != std::wstring::npos) refPath.replace(pos, 2, L"5}"); - if (SUCCEEDED(reader->GetMetadataByName(refPath.c_str(), &altRef))) { - if (altRef.vt == VT_UI1 && altRef.bVal == 1) pMetadata->Altitude = -alt; - PropVariantClear(&altRef); - } - break; - } - } - } - - return S_OK; -} - -HRESULT CImageLoader::ComputeHistogram(IWICBitmapSource* source, ImageMetadata* pMetadata) { - if (!source || !pMetadata) return E_INVALIDARG; - - // Reset - pMetadata->HistR.assign(256, 0); - pMetadata->HistG.assign(256, 0); - pMetadata->HistB.assign(256, 0); - pMetadata->HistL.assign(256, 0); - - ComPtr bitmap; - if (FAILED(source->QueryInterface(IID_PPV_ARGS(&bitmap)))) { - return E_NOINTERFACE; - } - - ComPtr lock; - if (FAILED(bitmap->Lock(nullptr, WICBitmapLockRead, &lock))) return E_FAIL; - - UINT width = 0, height = 0; - bitmap->GetSize(&width, &height); - - UINT stride = 0; - lock->GetStride(&stride); - - UINT bufSize = 0; - BYTE* ptr = nullptr; - if (FAILED(lock->GetDataPointer(&bufSize, &ptr))) return E_FAIL; - - WICPixelFormatGUID format; - source->GetPixelFormat(&format); - - // Determine offsets - int offsetR = 0, offsetG = 1, offsetB = 2; // Default RGBA - if (IsEqualGUID(format, GUID_WICPixelFormat32bppBGRA) || - IsEqualGUID(format, GUID_WICPixelFormat32bppPBGRA)) { - offsetR = 2; offsetG = 1; offsetB = 0; - } - - // Optimized Histogram Calculation - // For small images (< 2MP), use Step=1 (Exact). - // For large images, use Step > 1 (Sparse). - - UINT64 totalPixels = (UINT64)width * height; - - UINT step = 1; - if (totalPixels > 2000000) { // 2MP threshold - // Target ~250k samples - step = (UINT)sqrt(totalPixels / 250000.0); - if (step < 1) step = 1; - } - - // Sampling Loop - for (UINT y = 0; y < height; y += step) { - BYTE* row = ptr + (UINT64)y * stride; // Use 64-bit math for stride offset safety - for (UINT x = 0; x < width; x += step) { - // Safety check for last pixel in row? - // x*4 should be < stride. x < width implies x*4 < width*4 <= stride. - // But if stride is tight... - - BYTE r = row[x * 4 + offsetR]; - BYTE g = row[x * 4 + offsetG]; - BYTE b = row[x * 4 + offsetB]; - - pMetadata->HistR[r]++; - pMetadata->HistG[g]++; - pMetadata->HistB[b]++; - - // Luma (Integer approx: 0.2126R + 0.7152G + 0.0722B) - // (54*R + 183*G + 18*B) / 256 - BYTE l = (BYTE)((54 * r + 183 * g + 19 * b) >> 8); - pMetadata->HistL[l]++; - } - } - - return S_OK; -} diff --git a/QuickView/ImageLoader.h b/QuickView/ImageLoader.h deleted file mode 100644 index d7bb1d2d..00000000 --- a/QuickView/ImageLoader.h +++ /dev/null @@ -1,165 +0,0 @@ -#pragma once -#include "pch.h" -#include - -/// -/// Image Loader -/// Uses WIC to load image files -/// -class CImageLoader { -public: - CImageLoader() = default; - ~CImageLoader() = default; - - /// - /// Initialize loader - /// - HRESULT Initialize(IWICImagingFactory* wicFactory); - - // --- Metadata Structure --- - struct ImageMetadata { - std::wstring Make; - std::wstring Model; - std::wstring Lens; - std::wstring ISO; // e.g. "ISO 100" - std::wstring Aperture; // e.g. "f/2.8" - std::wstring Shutter; // e.g. "1/500s" - std::wstring Focal; // e.g. "50mm" - std::wstring ExposureBias; // e.g. "+0.3 EV" - std::wstring Flash; // New: Flash status - std::wstring Date; // EXIF Date or File Date fallback - std::wstring Software; // New: Software/Firmware - - UINT Width = 0; - UINT Height = 0; - UINT64 FileSize = 0; - std::wstring Format; // e.g. "JPEG", "RAW (ARW)" - std::wstring FormatDetails; // e.g. "4:2:0", "10-bit", "Lossy" - std::wstring ColorSpace; // e.g. "sRGB", "Display P3", "Adobe RGB" - - // Decoder Info - std::wstring LoaderName; // e.g. "TurboJPEG", "libavif" - DWORD LoadTimeMs = 0; // Load time in milliseconds - - // GPS - bool HasGPS = false; - double Latitude = 0.0; - double Longitude = 0.0; - double Altitude = 0.0; // New: Altitude - - // Histogram (256 bins) - std::vector HistR; - std::vector HistG; - std::vector HistB; - std::vector HistL; // Luminance - - bool IsEmpty() const { return Make.empty() && Model.empty() && ISO.empty() && Date.empty(); } - - std::wstring GetCompactString() const { - std::wstring s; - if (!Make.empty()) s += Make + L" "; - if (!Model.empty()) s += Model; - if (!Focal.empty()) s += (s.empty() ? L"" : L" ") + Focal; - if (!ISO.empty()) s += (s.empty() ? L"" : L" ") + (L"ISO " + ISO); - if (!Aperture.empty()) s += L" " + Aperture; - if (!Shutter.empty()) s += L" " + Shutter; - if (!ExposureBias.empty()) s += L" " + ExposureBias; - return s; - } - }; - - // --- NEW: Raw Thumbnail Data (Zero-Copy flow) --- - struct ThumbData { - std::vector pixels; // Raw BGRA (Compatible with PBGRA/BGRX) - int width; - int height; - int stride; - bool isValid = false; - - // Metadata for Hover - int origWidth = 0; - int origHeight = 0; - uint64_t fileSize = 0; - }; - - /// - /// Read metadata from file using WIC - /// - HRESULT ReadMetadata(LPCWSTR filePath, ImageMetadata* pMetadata); - - /// - /// Compute Histogram from bitmap (Sparse Sampling supported) - /// - HRESULT ComputeHistogram(IWICBitmapSource* source, ImageMetadata* pMetadata); - - /// - /// Load WIC bitmap from file - /// - HRESULT LoadFromFile(LPCWSTR filePath, IWICBitmapSource** bitmap); - - /// - /// Load WIC bitmap from file and force decode to memory - /// - HRESULT LoadToMemory(LPCWSTR filePath, IWICBitmap** ppBitmap, std::wstring* pLoaderName = nullptr, bool forceFullDecode = false); // Force decode to memory - - /// - /// NEW: Load Thumbnail (Raw Data) - /// Optimizes for speed using TurboJPEG scaling where possible. - /// Returns raw BGRA buffer. - /// - HRESULT LoadThumbnail(LPCWSTR filePath, int targetSize, ThumbData* pData); - - - /// - /// Get format details from last load (e.g. "4:2:0", "10-bit") - /// - std::wstring GetLastFormatDetails() const; - - /// - /// Get image size without full decode - /// - HRESULT GetImageSize(LPCWSTR filePath, UINT* width, UINT* height); - - // Helper: Create WIC bitmap from raw bits - HRESULT CreateWICBitmapFromMemory(UINT width, UINT height, REFGUID format, UINT stride, UINT size, BYTE* data, IWICBitmap** ppBitmap); - -private: - ComPtr m_wicFactory; - - // Specialized High-Performance Loaders - HRESULT LoadJPEG(LPCWSTR filePath, IWICBitmap** ppBitmap); // libjpeg-turbo - HRESULT LoadThumbJPEG(LPCWSTR filePath, int targetSize, ThumbData* pData); // New TurboJPEG Scaled Loader - HRESULT LoadThumbJPEGFromMemory(const uint8_t* pBuf, size_t size, int targetSize, ThumbData* pData); // Helper for in-memory buffers - HRESULT LoadThumbWebPFromMemory(const uint8_t* pBuf, size_t size, int targetSize, ThumbData* pData); // Helper for WebP buffers - - // LoadPNG REMOVED - replaced by LoadPngWuffs - HRESULT LoadWebP(LPCWSTR filePath, IWICBitmap** ppBitmap); // libwebp - HRESULT LoadAVIF(LPCWSTR filePath, IWICBitmap** ppBitmap); // libavif + dav1d - HRESULT LoadJXL(LPCWSTR filePath, IWICBitmap** ppBitmap); // libjxl - HRESULT LoadRaw(LPCWSTR filePath, IWICBitmap** ppBitmap, bool forceFullDecode); // libraw - - // Wuffs (Google's memory-safe decoder) - Ultimate Performance - HRESULT LoadPngWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); // Wuffs PNG (replaces libpng) - HRESULT LoadGifWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); // Wuffs GIF (replaces WIC) - HRESULT LoadBmpWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); // Wuffs BMP (Safer than WIC) - HRESULT LoadTgaWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); // Wuffs TGA (New support) - HRESULT LoadWbmpWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); // Wuffs WBMP (Fix sample) - - // Stb Image (Legacy/Special Formats: PSD, HDR, PIC, PNM) - HRESULT LoadStbImage(LPCWSTR filePath, IWICBitmap** ppBitmap, bool floatFormat = false); - - // TinyEXR (OpenEXR) - HRESULT LoadTinyExrImage(LPCWSTR filePath, IWICBitmap** ppBitmap); - - // NanoSVG (SVG) - HRESULT LoadSVG(LPCWSTR filePath, IWICBitmap** ppBitmap); - - // NetPBM via Wuffs (PAM, PBM, PGM, PPM) - HRESULT LoadNetpbmWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); - - // QOI via Wuffs - HRESULT LoadQoiWuffs(LPCWSTR filePath, IWICBitmap** ppBitmap); - - // Custom PCX Decoder - HRESULT LoadPCX(LPCWSTR filePath, IWICBitmap** ppBitmap); -}; diff --git a/QuickView/LosslessTransform.cpp b/QuickView/LosslessTransform.cpp deleted file mode 100644 index 3a5df210..00000000 --- a/QuickView/LosslessTransform.cpp +++ /dev/null @@ -1,489 +0,0 @@ -#include "pch.h" -#include "LosslessTransform.h" - -// Include libjpeg-turbo header -// Include libjpeg-turbo header -#include - - -// JPEG magic bytes: FF D8 FF -static const unsigned char JPEG_MAGIC[] = { 0xFF, 0xD8, 0xFF }; -// PNG magic bytes: 89 50 4E 47 -static const unsigned char PNG_MAGIC[] = { 0x89, 0x50, 0x4E, 0x47 }; - -// Helper: Read file into memory -static unsigned char* ReadFileToMemory(LPCWSTR filePath, unsigned int& lengthBytes) { - const unsigned int MAX_FILE_SIZE = 1024 * 1024 * 100; // 100 MB - - lengthBytes = 0; - HANDLE hFile = ::CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, 0, NULL); - if (hFile == INVALID_HANDLE_VALUE) { - return nullptr; - } - - LARGE_INTEGER fileSize; - if (!::GetFileSizeEx(hFile, &fileSize) || fileSize.QuadPart > MAX_FILE_SIZE) { - ::CloseHandle(hFile); - return nullptr; - } - - unsigned char* buffer = new(std::nothrow) unsigned char[static_cast(fileSize.QuadPart)]; - if (!buffer) { - ::CloseHandle(hFile); - return nullptr; - } - - DWORD bytesRead; - if (!::ReadFile(hFile, buffer, static_cast(fileSize.QuadPart), &bytesRead, NULL) || - bytesRead != fileSize.QuadPart) { - delete[] buffer; - ::CloseHandle(hFile); - return nullptr; - } - - ::CloseHandle(hFile); - lengthBytes = static_cast(fileSize.QuadPart); - return buffer; -} - -// Helper: Read first N bytes of file to check magic -static bool ReadFileMagic(LPCWSTR filePath, unsigned char* buffer, size_t count) { - HANDLE hFile = ::CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, 0, NULL); - if (hFile == INVALID_HANDLE_VALUE) { - return false; - } - - DWORD bytesRead; - bool ok = ::ReadFile(hFile, buffer, static_cast(count), &bytesRead, NULL) && - bytesRead == count; - - ::CloseHandle(hFile); - return ok; -} - -// Helper: Write memory to file (with safe temp file approach) -static bool WriteMemoryToFile(LPCWSTR filePath, unsigned char* buffer, unsigned int lengthBytes, - std::wstring& errorDetail) { - errorDetail.clear(); - - // Get ALL original file times (create, access, modify) - FILETIME ftCreate, ftAccess, ftModify; - bool restoreTime = false; - HANDLE hExisting = ::CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, 0, NULL); - if (hExisting != INVALID_HANDLE_VALUE) { - restoreTime = ::GetFileTime(hExisting, &ftCreate, &ftAccess, &ftModify) != 0; - ::CloseHandle(hExisting); - } - - // Write to temp file first for safety - std::wstring tempPath = std::wstring(filePath) + L".tmp"; - HANDLE hFile = ::CreateFileW(tempPath.c_str(), GENERIC_WRITE, 0, - NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (hFile == INVALID_HANDLE_VALUE) { - DWORD err = GetLastError(); - errorDetail = L"Cannot create temp file (error " + std::to_wstring(err) + L")"; - return false; - } - - DWORD bytesWritten; - bool ok = ::WriteFile(hFile, buffer, lengthBytes, &bytesWritten, NULL) && - bytesWritten == lengthBytes; - - // Restore ALL original timestamps (create, access, modify) - if (ok && restoreTime) { - ::SetFileTime(hFile, &ftCreate, &ftAccess, &ftModify); - } - - ::CloseHandle(hFile); - - if (!ok) { - ::DeleteFileW(tempPath.c_str()); - errorDetail = L"Failed to write temp file"; - return false; - } - - // Replace original with temp using MoveFileEx with REPLACE_EXISTING - // This is atomic on NTFS and handles file locking better - if (!::MoveFileExW(tempPath.c_str(), filePath, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { - DWORD err = GetLastError(); - - // Try alternative: delete original first then move - if (::DeleteFileW(filePath)) { - if (::MoveFileW(tempPath.c_str(), filePath)) { - return true; // Success via fallback path - } - } - - // Failed - clean up temp file - ::DeleteFileW(tempPath.c_str()); - - switch (err) { - case ERROR_ACCESS_DENIED: - errorDetail = L"Access denied - file may be in use or read-only"; - break; - case ERROR_SHARING_VIOLATION: - errorDetail = L"File is locked by another process"; - break; - default: - errorDetail = L"Cannot replace file (error " + std::to_wstring(err) + L")"; - break; - } - return false; - } - - return true; -} - -// Helper: Convert TransformType to TurboJPEG operation code -static int TransformTypeToTJOp(TransformType type) { - switch (type) { - case TransformType::Rotate90CW: return TJXOP_ROT90; - case TransformType::Rotate90CCW: return TJXOP_ROT270; - case TransformType::Rotate180: return TJXOP_ROT180; - case TransformType::FlipHorizontal: return TJXOP_HFLIP; - case TransformType::FlipVertical: return TJXOP_VFLIP; - default: return TJXOP_NONE; - } -} - -bool CLosslessTransform::IsJPEG(LPCWSTR filePath) { - if (!filePath) return false; - - // Check file content magic bytes, not just extension - unsigned char magic[4] = { 0 }; - if (!ReadFileMagic(filePath, magic, 4)) { - return false; - } - - // JPEG starts with FF D8 FF - return (magic[0] == JPEG_MAGIC[0] && - magic[1] == JPEG_MAGIC[1] && - magic[2] == JPEG_MAGIC[2]); -} - -bool CLosslessTransform::IsJPEGByExtension(LPCWSTR filePath) { - if (!filePath) return false; - - std::wstring path(filePath); - size_t dotPos = path.rfind(L'.'); - if (dotPos == std::wstring::npos) return false; - - std::wstring ext = path.substr(dotPos); - // Convert to lowercase - for (auto& c : ext) c = towlower(c); - - return (ext == L".jpg" || ext == L".jpeg" || ext == L".jpe"); -} - -TransformResult CLosslessTransform::TransformJPEG( - LPCWSTR inputPath, - LPCWSTR outputPath, - TransformType type) { - - if (!inputPath || !outputPath) { - return TransformResult::Error(L"Invalid file path"); - } - - // Read input file - unsigned int inputSize; - unsigned char* inputData = ReadFileToMemory(inputPath, inputSize); - if (!inputData) { - return TransformResult::Error(L"Failed to read file"); - } - - // Verify it's actually a JPEG by checking magic bytes - if (inputSize < 3 || - inputData[0] != JPEG_MAGIC[0] || - inputData[1] != JPEG_MAGIC[1] || - inputData[2] != JPEG_MAGIC[2]) { - delete[] inputData; - - // Check if it's a PNG masquerading as JPEG - if (inputSize >= 4 && - inputData[0] == PNG_MAGIC[0] && - inputData[1] == PNG_MAGIC[1]) { - return TransformResult::Error(L"File is PNG (not JPEG) - using generic transform"); - } - - return TransformResult::Error(L"Not a valid JPEG file"); - } - - // Initialize TurboJPEG transformer - tjhandle tjHandle = tj3Init(TJINIT_TRANSFORM); - if (!tjHandle) { - delete[] inputData; - return TransformResult::Error(L"Failed to initialize JPEG transformer"); - } - - // Set up transformation - tjtransform transform = { 0 }; - transform.op = TransformTypeToTJOp(type); - - // First try PERFECT transformation (lossless) - transform.options = TJXOPT_PERFECT; - - // Perform transformation - unsigned char* outputData = nullptr; - size_t outputSize = 0; - - EditQuality quality = EditQuality::Lossless; - - int result = tj3Transform(tjHandle, inputData, inputSize, 1, - &outputData, &outputSize, &transform); - - // If perfect transform failed due to MCU alignment, try TRIM - if (result != 0) { - const char* errStr = tj3GetErrorStr(tjHandle); - // Check for "perfect" or "MCU" in error message - // libjpeg-turbo usually says "Transform is not perfect" - if (errStr && (strstr(errStr, "perfect") != nullptr || strstr(errStr, "MCU") != nullptr)) { - // Retry with TRIM - transform.options = TJXOPT_TRIM; - result = tj3Transform(tjHandle, inputData, inputSize, 1, - &outputData, &outputSize, &transform); - - if (result == 0) { - quality = EditQuality::EdgeAdapted; - } - } - } - - delete[] inputData; - - TransformResult transformResult; - - if (result == 0 && outputData) { - // Write output file - std::wstring writeError; - if (WriteMemoryToFile(outputPath, outputData, static_cast(outputSize), writeError)) { - transformResult = TransformResult::OK(quality); - } else { - transformResult = TransformResult::Error(writeError); - } - tj3Free(outputData); - } else { - std::wstring errMsg = L"JPEG transformation failed"; - const char* errStr = tj3GetErrorStr(tjHandle); - if (errStr) { - // Convert error string to wide - int len = MultiByteToWideChar(CP_UTF8, 0, errStr, -1, NULL, 0); - if (len > 0) { - std::wstring wideErr(len, 0); - MultiByteToWideChar(CP_UTF8, 0, errStr, -1, &wideErr[0], len); - errMsg += L": " + wideErr; - } - } - transformResult = TransformResult::Error(errMsg); - } - - tj3Destroy(tjHandle); - return transformResult; -} - -TransformResult CLosslessTransform::TransformGeneric( - LPCWSTR inputPath, - LPCWSTR outputPath, - TransformType type) { - - // For non-JPEG files, use WIC to decode, transform, and re-encode - // PNG/BMP/GIF are lossless formats - re-encoding preserves quality - // JPEG/WebP lossy would lose quality - - HRESULT hr = S_OK; - - // Read original file times first - FILETIME ftCreate, ftAccess, ftModify; - bool restoreTime = false; - HANDLE hTimeFile = ::CreateFileW(inputPath, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, 0, NULL); - if (hTimeFile != INVALID_HANDLE_VALUE) { - restoreTime = ::GetFileTime(hTimeFile, &ftCreate, &ftAccess, &ftModify) != 0; - ::CloseHandle(hTimeFile); - } - - ComPtr wicFactory; - hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&wicFactory)); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to create WIC factory"); - } - - // Load image with cache on load to release file handle early - ComPtr decoder; - hr = wicFactory->CreateDecoderFromFilename(inputPath, nullptr, GENERIC_READ, - WICDecodeMetadataCacheOnLoad, &decoder); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to load image"); - } - - ComPtr frame; - hr = decoder->GetFrame(0, &frame); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to get image frame"); - } - - // Get original encoder CLSID before releasing decoder - GUID containerFormat; - hr = decoder->GetContainerFormat(&containerFormat); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to get image format"); - } - - // Determine format name for message - bool isLosslessFormat = (containerFormat == GUID_ContainerFormatPng || - containerFormat == GUID_ContainerFormatBmp || - containerFormat == GUID_ContainerFormatGif || - containerFormat == GUID_ContainerFormatTiff); - - // Cache the image in memory to release the file handle - ComPtr cachedBitmap; - hr = wicFactory->CreateBitmapFromSource(frame.Get(), WICBitmapCacheOnLoad, &cachedBitmap); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to cache image in memory"); - } - - // Release decoder and frame to free file handle - frame.Reset(); - decoder.Reset(); - - // Determine WIC rotation - WICBitmapTransformOptions wicTransform; - switch (type) { - case TransformType::Rotate90CW: wicTransform = WICBitmapTransformRotate90; break; - case TransformType::Rotate90CCW: wicTransform = WICBitmapTransformRotate270; break; - case TransformType::Rotate180: wicTransform = WICBitmapTransformRotate180; break; - case TransformType::FlipHorizontal: wicTransform = WICBitmapTransformFlipHorizontal; break; - case TransformType::FlipVertical: wicTransform = WICBitmapTransformFlipVertical; break; - default: wicTransform = WICBitmapTransformRotate0; break; - } - - // Apply rotation using WIC FlipRotator on the cached bitmap - ComPtr rotator; - hr = wicFactory->CreateBitmapFlipRotator(&rotator); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to create rotator"); - } - - hr = rotator->Initialize(cachedBitmap.Get(), wicTransform); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to apply rotation"); - } - - // For in-place transform, we need to use a temp file - std::wstring tempPath = std::wstring(outputPath) + L".wic.tmp"; - - // Create encoder - ComPtr encoder; - hr = wicFactory->CreateEncoder(containerFormat, nullptr, &encoder); - if (FAILED(hr)) { - // Fallback to PNG - hr = wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder); - isLosslessFormat = true; // PNG is lossless - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to create encoder"); - } - } - - // Create output stream - ComPtr stream; - hr = wicFactory->CreateStream(&stream); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to create output stream"); - } - - hr = stream->InitializeFromFilename(tempPath.c_str(), GENERIC_WRITE); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to open output file"); - } - - hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to initialize encoder"); - } - - // Create frame - ComPtr frameEncode; - hr = encoder->CreateNewFrame(&frameEncode, nullptr); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to create output frame"); - } - - hr = frameEncode->Initialize(nullptr); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to initialize output frame"); - } - - // Write rotated image - UINT width, height; - rotator->GetSize(&width, &height); - hr = frameEncode->SetSize(width, height); - if (FAILED(hr)) { - return TransformResult::Error(L"Failed to set output size"); - } - - // Get pixel format - WICPixelFormatGUID pixelFormat; - rotator->GetPixelFormat(&pixelFormat); - hr = frameEncode->SetPixelFormat(&pixelFormat); - - hr = frameEncode->WriteSource(rotator.Get(), nullptr); - if (FAILED(hr)) { - ::DeleteFileW(tempPath.c_str()); - return TransformResult::Error(L"Failed to write output image"); - } - - hr = frameEncode->Commit(); - if (FAILED(hr)) { - ::DeleteFileW(tempPath.c_str()); - return TransformResult::Error(L"Failed to commit output frame"); - } - - hr = encoder->Commit(); - if (FAILED(hr)) { - ::DeleteFileW(tempPath.c_str()); - return TransformResult::Error(L"Failed to commit output file"); - } - - // Close the stream before moving - stream.Reset(); - encoder.Reset(); - frameEncode.Reset(); - - // Restore original timestamps on temp file before move - if (restoreTime) { - HANDLE hOutFile = ::CreateFileW(tempPath.c_str(), FILE_WRITE_ATTRIBUTES, 0, - NULL, OPEN_EXISTING, 0, NULL); - if (hOutFile != INVALID_HANDLE_VALUE) { - ::SetFileTime(hOutFile, &ftCreate, &ftAccess, &ftModify); - ::CloseHandle(hOutFile); - } - } - - // Move temp file to output - if (!::MoveFileExW(tempPath.c_str(), outputPath, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { - DWORD err = GetLastError(); - ::DeleteFileW(tempPath.c_str()); - return TransformResult::Error(L"Failed to save file (error " + std::to_wstring(err) + L")"); - } - - // Return appropriate quality based on format - if (isLosslessFormat) { - return TransformResult::OK(EditQuality::LosslessReenc); - } else { - return TransformResult::OK(EditQuality::Lossy); - } -} - -const wchar_t* CLosslessTransform::GetTransformName(TransformType type) { - switch (type) { - case TransformType::Rotate90CW: return L"Rotate 90\x00B0 CW"; - case TransformType::Rotate90CCW: return L"Rotate 90\x00B0 CCW"; - case TransformType::Rotate180: return L"Rotate 180\x00B0"; - case TransformType::FlipHorizontal: return L"Flip Horizontal"; - case TransformType::FlipVertical: return L"Flip Vertical"; - default: return L"Unknown"; - } -} diff --git a/QuickView/LosslessTransform.h b/QuickView/LosslessTransform.h deleted file mode 100644 index b7847b8b..00000000 --- a/QuickView/LosslessTransform.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once -#include "pch.h" - -/// -/// Lossless transformation types -/// -enum class TransformType { - Rotate90CW, // Clockwise 90 degrees - Rotate90CCW, // Counter-clockwise 90 degrees - Rotate180, // 180 degrees - FlipHorizontal, // Horizontal flip (mirror) - FlipVertical // Vertical flip -}; - -/// -/// Edit quality after rotation/flip -/// -enum class EditQuality { - Lossless, // Green - JPEG lossless - LosslessReenc, // Green - PNG/BMP re-encoded but lossless - EdgeAdapted, // Yellow - JPEG edge adapted (trimmed for MCU alignment) - Lossy // Red - Lossy re-encode (WebP, etc.) -}; - -/// -/// Result of a transformation operation -/// -struct TransformResult { - bool Success = false; - std::wstring ErrorMessage; - EditQuality Quality = EditQuality::Lossless; - - static TransformResult OK(EditQuality quality) { - return { true, L"", quality }; - } - - static TransformResult Error(const std::wstring& msg) { - return { false, msg, EditQuality::Lossless }; - } -}; - -/// -/// Lossless image transformation using libjpeg-turbo -/// Supports JPEG lossless rotation and flip operations -/// -class CLosslessTransform { -public: - /// - /// Check if file is JPEG format by reading magic bytes - /// - static bool IsJPEG(LPCWSTR filePath); - - /// - /// Check if file has JPEG extension (may not be actual JPEG) - /// - static bool IsJPEGByExtension(LPCWSTR filePath); - - /// - /// Perform lossless JPEG transformation - /// Input and output can be the same file (in-place transform) - /// - static TransformResult TransformJPEG( - LPCWSTR inputPath, - LPCWSTR outputPath, - TransformType type); - - /// - /// Perform transformation on any image format using WIC (lossy for non-JPEG) - /// - static TransformResult TransformGeneric( - LPCWSTR inputPath, - LPCWSTR outputPath, - TransformType type); - - /// - /// Get human-readable name for transform type - /// - static const wchar_t* GetTransformName(TransformType type); -}; diff --git a/QuickView/OSDState.h b/QuickView/OSDState.h deleted file mode 100644 index a8962d3b..00000000 --- a/QuickView/OSDState.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include -#include -#include - -struct OSDState { - std::wstring Message; - DWORD StartTime = 0; - DWORD Duration = 2000; - bool IsError = false; - bool IsWarning = false; - D2D1_COLOR_F CustomColor = D2D1::ColorF(D2D1::ColorF::Black, 0.0f); - - void Show(HWND hwnd, const std::wstring& msg, bool error = false, bool warning = false, D2D1_COLOR_F color = D2D1::ColorF(D2D1::ColorF::Black, 0.0f)) { - Message = msg; - StartTime = GetTickCount(); - IsError = error; - IsWarning = warning; - CustomColor = color; - if (hwnd) { - SetTimer(hwnd, 999, 250, nullptr); - InvalidateRect(hwnd, nullptr, FALSE); - } - } - - bool IsVisible() const { - return !Message.empty() && (GetTickCount() - StartTime) < Duration; - } -}; diff --git a/QuickView/PreviewExtractor.cpp b/QuickView/PreviewExtractor.cpp deleted file mode 100644 index 2b359a6c..00000000 --- a/QuickView/PreviewExtractor.cpp +++ /dev/null @@ -1,244 +0,0 @@ -#include "pch.h" -#include "PreviewExtractor.h" -#include -#include - -// Helper Macros -#define U16LE(p) (*(const uint16_t*)(p)) -#define U32LE(p) (*(const uint32_t*)(p)) -#define U16BE(p) (uint16_t)((p)[0]<<8 | (p)[1]) -#define U32BE(p) (uint32_t)((p)[0]<<24 | (p)[1]<<16 | (p)[2]<<8 | (p)[3]) - -// --- RAW (TIFF-based) --- - -bool PreviewExtractor::ExtractFromRAW(const uint8_t* data, size_t size, ExtractedData& out) { - if (size < 1024) return false; - - // TIFF Header check - // II (49 49) = Little Endian - // MM (4D 4D) = Big Endian - bool isLE = false; - if (data[0] == 0x49 && data[1] == 0x49) isLE = true; - else if (data[0] == 0x4D && data[1] == 0x4D) isLE = false; - else return false; // Not TIFF - - uint16_t fortyTwo = isLE ? U16LE(data + 2) : U16BE(data + 2); - if (fortyTwo != 42 && fortyTwo != 0x55 && fortyTwo != 0x4F52) { // 42, Panasonic(85), Olympus(0x4F52) - // Check for CR3? CR3 is ISOBMFF, not TIFF. - // If CR3, this check fails, handled by HEIC parser (similar structure) or specific CR3 logic. - // But Canon CR2 is TIFF. - // Let's assume TIFF standard for now. - } - - uint32_t firstIFD = isLE ? U32LE(data + 4) : U32BE(data + 4); - if (firstIFD >= size) return false; - - uint64_t jpgOff = 0, jpgSz = 0; - - // Parse IFD0 - // IFD0 usually contains the tiny thumbnail (160x120). - // SubIFDs usually contain the full preview. - // Sony ARW: Preview in SubIFD. - // Canon CR2: Preview image tag in IFD0 or StripOffsets? - - // Strategy: Look for "JpgFromRawStart" (Tag 0x0201) or "PreviewImageStart" - // Also check SubIFDs (Tag 0x014A) - - if (ParseTiffIFD(data, size, firstIFD, isLE, jpgOff, jpgSz)) { - if (jpgOff > 0 && jpgSz > 1024 && jpgOff + jpgSz <= size) { - out.pData = data + jpgOff; - out.size = (size_t)jpgSz; - return true; - } - } - - return false; -} - -bool PreviewExtractor::ParseTiffIFD(const uint8_t* data, size_t size, size_t offset, bool isLE, uint64_t& jpegOffset, uint64_t& jpegSize) { - if (offset + 2 > size) return false; - - uint16_t count = isLE ? U16LE(data + offset) : U16BE(data + offset); - size_t entrySize = 12; - size_t current = offset + 2; - - if (current + count * entrySize > size) return false; - - uint64_t subIfdOffset = 0; - - for (int i = 0; i < count; i++) { - const uint8_t* ev = data + current + i * entrySize; - uint16_t tag = isLE ? U16LE(ev) : U16BE(ev); - uint16_t type = isLE ? U16LE(ev + 2) : U16BE(ev + 2); - uint32_t cnt = isLE ? U32LE(ev + 4) : U32BE(ev + 4); - uint32_t valOrOff = isLE ? U32LE(ev + 8) : U32BE(ev + 8); // Value if fits 4 bytes, else offset - - // JPEGInterchangeFormat (0x0201) - Common in EXIF/TIFF, points to SOI - if (tag == 0x0201) { - jpegOffset = valOrOff; - } - // JPEGInterchangeFormatLength (0x0202) - else if (tag == 0x0202) { - jpegSize = valOrOff; - } - // StripOffsets (0x0111) - Sometimes used for thumb - // StripByteCounts (0x0117) - // SubIFDs (0x014A) - VERY IMPORTANT for RAW - else if (tag == 0x014A) { - // SubIFDs usually points to a list of offsets. - // If type=4 (LONG), cnt=1, then valOrOff is offset. - // If cnt > 1, valOrOff is offset to array of offsets. - if (type == 4 || type == 3) { // LONG or SHORT - if (cnt == 1) subIfdOffset = valOrOff; - else subIfdOffset = valOrOff; // Offset to array - } - } - } - - // If found valid JPEG in this IFD, check if it's "large enough" (> 50KB?) - // Thumbnails are small. Previews are large. - if (jpegOffset > 0 && jpegSize > 50000) return true; - - // Recurse into SubIFDs if current wasn't good - if (jpegOffset == 0 && subIfdOffset > 0 && subIfdOffset < size) { - // Assume subIfdOffset points to first SubIFD or array - // For simplicity, just try to parse the offset itself as an IFD - // Correct logic is complex (array reading). - // Hack: Try to parse whatever is at subIfdOffset. - // Usually it's an array of LONGs. Read first one. - // If SubIFDS tag pointed to array, we need to read memory. - // Let's assume it points to valid IFD. - - // Actually for ARW/DNG, SubIFD is main image or preview. - // Let's implement full parsing later. - // For now, let's enable DNG/ARW basic detection. - } - - return (jpegOffset > 0 && jpegSize > 0); -} - - -// --- HEIC (ISOBMFF) --- -uint32_t PreviewExtractor::ReadU32BE(const uint8_t* p) { return U32BE(p); } - -bool PreviewExtractor::ExtractFromHEIC(const uint8_t* data, size_t size, ExtractedData& out) { - // Simplified ISOBMFF Parser - // Look for 'meta' box -> 'iloc' box -> item info - // HEIC usually stores thumb as a separate item, or main image (grid) - // Finding extracted JPEG (Exif thumb) inside 'meta' -> 'Exif' item? - // Often HEIC contains a small AVC/HEVC thumbnail track. - // BUT, we want JPEG. - // Many camera HEIC embed Exif. Exif has JPEG thumb. - - // Quick Scan for Exif Marker? - // Exif in ISOBMFF is wrapped in 'Exif' item. - // Scanning for "Exif\0\0" (Standard Exif header) might work. - - if (size < 1024) return false; - - const uint8_t exifSig[] = { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 }; - // Brute force scan for Exif header (fast enough for memory mapped file) - // Limit scan to first 1MB? - size_t scanLimit = std::min(size, (size_t)512 * 1024); - - for (size_t i = 0; i < scanLimit - 6; i++) { - if (std::memcmp(data + i, exifSig, 6) == 0) { - // Found Exif Block - // Parse Exif (TIFF format) - // Exif header usually follows 49 49 or 4D 4D within 6 bytes? - // Standard: Exif\0\0 + TIFF Header - size_t tiffStart = i + 6; - if (tiffStart + 8 > size) continue; - - uint64_t jpgOff = 0, jpgSz = 0; - bool isLE = (data[tiffStart] == 0x49); - - uint32_t ifd0 = isLE ? U32LE(data + tiffStart + 4) : U32BE(data + tiffStart + 4); - - if (ParseTiffIFD(data + tiffStart, size - tiffStart, ifd0, isLE, jpgOff, jpgSz)) { - if (jpgOff > 0 && jpgSz > 0) { - out.pData = data + tiffStart + jpgOff; - out.size = (size_t)jpgSz; - return true; - } - } - } - } - return false; -} - -// --- PSD --- - -bool PreviewExtractor::ExtractFromPSD(const uint8_t* data, size_t size, ExtractedData& out) { - if (size < 26) return false; - if (std::memcmp(data, "8BPS", 4) != 0) return false; - - // Header (26 bytes) - // Channels (2 bytes), Height (4), Width (4), Depth (2), Mode (2) - - // Color Mode Data Section - size_t offset = 26; - uint32_t colorModeLen = U32BE(data + offset); - offset += 4 + colorModeLen; - if (offset >= size) return false; - - // Image Resources Section - if (offset + 4 > size) return false; - uint32_t imgResLen = U32BE(data + offset); - size_t resEnd = offset + 4 + imgResLen; - offset += 4; - - if (resEnd > size) return false; - - // Parse Resources - while (offset < resEnd) { - if (offset + 6 > resEnd) break; - // Signature 8BIM - if (std::memcmp(data + offset, "8BIM", 4) != 0) break; - offset += 4; - - uint16_t id = U16BE(data + offset); - offset += 2; - - // Name (Pascal String, padded to even) - uint8_t nameLen = data[offset]; - offset += 1 + nameLen; - if (offset % 2 != 0) offset++; // Pad - - if (offset + 4 > resEnd) break; - uint32_t sizeData = U32BE(data + offset); - offset += 4; - - if (offset + sizeData > resEnd) break; - - // ID 1033 (Thumbnail resource 1) or 1036 (Thumbnail resource 2 - preferred) - if (id == 1036 || id == 1033) { - // Found thumbnail resource! - // Format: - // 4 bytes: Format (1 = kJpegRGB) - // 4 bytes: Width - // 4 bytes: Height - // 4 bytes: WidthBytes - // 4 bytes: TotalSize - // 4 bytes: SizeCompressed - // 2 bytes: BitsPerPixel - // 2 bytes: Planes - // Data... - - if (sizeData > 28) { - uint32_t format = U32BE(data + offset); - if (format == 1) { // JPEG - // JPEG data starts at offset + 28 - out.pData = data + offset + 28; - out.size = sizeData - 28; - return true; - } - } - } - - offset += sizeData; - if (offset % 2 != 0) offset++; // Pad - } - - return false; -} diff --git a/QuickView/PreviewExtractor.h b/QuickView/PreviewExtractor.h deleted file mode 100644 index 0b41192c..00000000 --- a/QuickView/PreviewExtractor.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include -#include -#include -#include - -// PreviewExtractor: Static helper to extract embedded thumbnails -class PreviewExtractor { -public: - struct ExtractedData { - const uint8_t* pData = nullptr; // Pointer to data within source buffer - size_t size = 0; - bool isCopy = false; // If true, caller owns pData (rare) - std::vector buffer; // Storage if copy needed - - bool IsValid() const { return pData != nullptr && size > 512; } // Min valid size - }; - - // RAW (ARW, CR2, NEF, DNG, RAF, ORF) - // All usually Tiff-based. - static bool ExtractFromRAW(const uint8_t* fileData, size_t fileSize, ExtractedData& out); - - // HEIC / HEIF / AVIF - // ISOBMFF based. - static bool ExtractFromHEIC(const uint8_t* fileData, size_t fileSize, ExtractedData& out); - - // PSD / PSB - // Resource block based. - static bool ExtractFromPSD(const uint8_t* fileData, size_t fileSize, ExtractedData& out); - -private: - // TIFF Parsing Helpers - static bool ParseTiffIFD(const uint8_t* start, size_t size, size_t offset, bool isLittleEndian, uint64_t& jpegOffset, uint64_t& jpegSize); - - // ISOBMFF Parsing Helpers - static uint32_t ReadU32BE(const uint8_t* p); - static uint64_t ReadU64BE(const uint8_t* p); -}; diff --git a/QuickView/QuickView.h b/QuickView/QuickView.h deleted file mode 100644 index e4b2411f..00000000 --- a/QuickView/QuickView.h +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -#define MAX_LOADSTRING 100 diff --git a/QuickView/QuickView.rc b/QuickView/QuickView.rc deleted file mode 100644 index 01d7a692..00000000 --- a/QuickView/QuickView.rc +++ /dev/null @@ -1,8 +0,0 @@ -// QuickView.rc - Resource file -// Application Icon - -#include - -// Main Application Icon (ID = 1 for default) -// IDI_APPLICATION will be overridden by this -1 ICON "..\\QuickView.ico" diff --git a/QuickView/QuickView.vcxproj b/QuickView/QuickView.vcxproj deleted file mode 100644 index 7c03009f..00000000 --- a/QuickView/QuickView.vcxproj +++ /dev/null @@ -1,145 +0,0 @@ - - - - - Debug - x64 - - - Release - x64 - - - - 17.0 - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} - QuickView - 10.0 - x64-windows-static-opt - true - - - - Application - true - v145 - Unicode - false - - - Application - false - v145 - true - Unicode - false - - - - - - - - - - - - - - $(ProjectDir)bin\x64\Debug\ - $(ProjectDir)obj\x64\Debug\ - - - $(ProjectDir)bin\x64\Release\ - $(ProjectDir)obj\x64\Release\ - - - - Level4 - true - _DEBUG;_WINDOWS;%(PreprocessorDefinitions) - true - stdcpplatest - Use - pch.h - MultiThreadedDebug - %(AdditionalIncludeDirectories) - ProgramDatabase - - - Windows - true - %(AdditionalLibraryDirectories) - dwrite.lib;%(AdditionalDependencies) - - - - - Level4 - true - true - true - NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - true - true - stdcpplatest - Use - pch.h - MultiThreaded - MaxSpeed - AdvancedVectorExtensions2 - Fast - %(AdditionalIncludeDirectories) - - - Windows - UseFastLinkTimeCodeGeneration - true - true - true - %(AdditionalLibraryDirectories) - dwrite.lib;%(AdditionalDependencies) - - - - - - - - - - - - - - - - - - - Create - Create - - - - - - - - NotUsing - NotUsing - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/QuickView/RenderEngine.cpp b/QuickView/RenderEngine.cpp deleted file mode 100644 index 70c4b457..00000000 --- a/QuickView/RenderEngine.cpp +++ /dev/null @@ -1,399 +0,0 @@ -#include "pch.h" -#include "RenderEngine.h" - -CRenderEngine::~CRenderEngine() { - // ComPtr automatically releases resources -} - -HRESULT CRenderEngine::Initialize(HWND hwnd) { - m_hwnd = hwnd; - - HRESULT hr = S_OK; - - // 1. Create WIC factory - hr = CoCreateInstance( - CLSID_WICImagingFactory, - nullptr, - CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&m_wicFactory) - ); - if (FAILED(hr)) return hr; - - // 1.5 Create DWrite Factory - hr = DWriteCreateFactory( - DWRITE_FACTORY_TYPE_SHARED, - __uuidof(IDWriteFactory), - (IUnknown**)(&m_dwriteFactory) - ); - if (FAILED(hr)) return hr; - - // Create standard text format - hr = m_dwriteFactory->CreateTextFormat( - L"Segoe UI", - nullptr, - DWRITE_FONT_WEIGHT_SEMI_BOLD, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 24.0f, - L"en-us", - &m_textFormat - ); - if (FAILED(hr)) return hr; - - // Center alignment - actually LEADING is better for manual layout calculation - m_textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); - m_textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); - - // 2. Create D3D11 device and D2D factory - hr = CreateDeviceResources(); - if (FAILED(hr)) return hr; - - // 3. Create SwapChain - hr = CreateSwapChain(hwnd); - if (FAILED(hr)) return hr; - - // 4. Create render target - hr = CreateRenderTarget(); - if (FAILED(hr)) return hr; - - return S_OK; -} - -HRESULT CRenderEngine::CreateDeviceResources() { - HRESULT hr = S_OK; - - // D3D11 device creation flags - UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; -#ifdef _DEBUG - creationFlags |= D3D11_CREATE_DEVICE_DEBUG; -#endif - - // Feature levels - D3D_FEATURE_LEVEL featureLevels[] = { - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0, - }; - - D3D_FEATURE_LEVEL featureLevel; - - // Create D3D11 device - hr = D3D11CreateDevice( - nullptr, // Default adapter - D3D_DRIVER_TYPE_HARDWARE, // Hardware acceleration - nullptr, // Software rasterizer - creationFlags, - featureLevels, - ARRAYSIZE(featureLevels), - D3D11_SDK_VERSION, - &m_d3dDevice, - &featureLevel, - &m_d3dContext - ); - if (FAILED(hr)) return hr; - - // Get DXGI device - ComPtr dxgiDevice; - hr = m_d3dDevice.As(&dxgiDevice); - if (FAILED(hr)) return hr; - - // Create D2D factory - D2D1_FACTORY_OPTIONS options = {}; -#ifdef _DEBUG - options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; -#endif - - hr = D2D1CreateFactory( - D2D1_FACTORY_TYPE_SINGLE_THREADED, - options, - m_d2dFactory.GetAddressOf() - ); - if (FAILED(hr)) return hr; - - // Create D2D device - hr = m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice); - if (FAILED(hr)) return hr; - - // Create D2D device context - hr = m_d2dDevice->CreateDeviceContext( - D2D1_DEVICE_CONTEXT_OPTIONS_NONE, - &m_d2dContext - ); - if (FAILED(hr)) return hr; - - return S_OK; -} - -HRESULT CRenderEngine::CreateSwapChain(HWND hwnd) { - HRESULT hr = S_OK; - - // Get DXGI factory - ComPtr dxgiDevice; - hr = m_d3dDevice.As(&dxgiDevice); - if (FAILED(hr)) return hr; - - ComPtr dxgiAdapter; - hr = dxgiDevice->GetAdapter(&dxgiAdapter); - if (FAILED(hr)) return hr; - - ComPtr dxgiFactory; - hr = dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory)); - if (FAILED(hr)) return hr; - - // SwapChain description - Flip Model - DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; - swapChainDesc.Width = 0; // Auto from HWND - swapChainDesc.Height = 0; - swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - swapChainDesc.Stereo = FALSE; - swapChainDesc.SampleDesc.Count = 1; - swapChainDesc.SampleDesc.Quality = 0; - swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - swapChainDesc.BufferCount = 2; // Double buffering - swapChainDesc.Scaling = DXGI_SCALING_STRETCH; - swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; - swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; - swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; - - // Create SwapChain (try with Waitable Object first) - ComPtr swapChain1; - hr = dxgiFactory->CreateSwapChainForHwnd( - m_d3dDevice.Get(), - hwnd, - &swapChainDesc, - nullptr, nullptr, - &swapChain1 - ); - - bool waitable = true; - if (FAILED(hr)) { - // Fallback: Try without Waitable Object - swapChainDesc.Flags = 0; - hr = dxgiFactory->CreateSwapChainForHwnd( - m_d3dDevice.Get(), - hwnd, - &swapChainDesc, - nullptr, nullptr, - &swapChain1 - ); - waitable = false; - } - if (FAILED(hr)) return hr; - - // Query IDXGISwapChain2 - hr = swapChain1.As(&m_swapChain); - if (FAILED(hr)) return hr; - - if (waitable) { - // Set Maximum Frame Latency to 1 (Min latency) - hr = m_swapChain->SetMaximumFrameLatency(1); - if (SUCCEEDED(hr)) { - m_frameLatencyWaitableObject = m_swapChain->GetFrameLatencyWaitableObject(); - } - } - - // Disable Alt+Enter fullscreen toggle - dxgiFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER); - - return S_OK; -} - -HRESULT CRenderEngine::CreateRenderTarget() { - HRESULT hr = S_OK; - - // Get SwapChain back buffer - ComPtr dxgiBackBuffer; - hr = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&dxgiBackBuffer)); - if (FAILED(hr)) return hr; - - // Create D2D bitmap properties - D2D1_BITMAP_PROPERTIES1 bitmapProperties = D2D1::BitmapProperties1( - D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE) - ); - - // Create D2D bitmap from DXGI surface - hr = m_d2dContext->CreateBitmapFromDxgiSurface( - dxgiBackBuffer.Get(), - &bitmapProperties, - &m_targetBitmap - ); - if (FAILED(hr)) return hr; - - // Set render target - m_d2dContext->SetTarget(m_targetBitmap.Get()); - - return S_OK; -} - -HRESULT CRenderEngine::Resize(UINT width, UINT height) { - if (!m_swapChain) return E_FAIL; - if (width == 0 || height == 0) return S_OK; - - // Release old render target - m_d2dContext->SetTarget(nullptr); - m_targetBitmap.Reset(); - - // Resize SwapChain - HRESULT hr = m_swapChain->ResizeBuffers( - 0, // Keep buffer count - width, - height, - DXGI_FORMAT_UNKNOWN, // Keep format - m_frameLatencyWaitableObject ? DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT : 0 - ); - if (FAILED(hr)) return hr; - - // Reset Latency and Re-query Waitable Object (Required after ResizeBuffers IF waitable) - if (m_frameLatencyWaitableObject) { - m_swapChain->SetMaximumFrameLatency(1); - m_frameLatencyWaitableObject = m_swapChain->GetFrameLatencyWaitableObject(); - if (!m_frameLatencyWaitableObject) return E_FAIL; - } - - // Recreate render target - return CreateRenderTarget(); -} - -void CRenderEngine::WaitForGPU() { - if (m_frameLatencyWaitableObject) { - DWORD result = WaitForSingleObjectEx(m_frameLatencyWaitableObject, 100, true); // Reduced timeout: 100ms - if (result == WAIT_TIMEOUT) { - // Timeout occurred - GPU may be stuck, skip waiting - // This prevents hang when D2D/WIC enters error state - } - } -} - -void CRenderEngine::BeginDraw() { - WaitForGPU(); - m_d2dContext->BeginDraw(); -} - -void CRenderEngine::Clear(const D2D1_COLOR_F& color) { - m_d2dContext->Clear(color); -} - -void CRenderEngine::DrawBitmap(ID2D1Bitmap* bitmap, const D2D1_RECT_F& destRect) { - if (!bitmap) return; - - // Default to high quality - caller handles dynamic mode selection if needed - m_d2dContext->DrawBitmap( - bitmap, - destRect, - 1.0f, // Opacity - D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC - ); -} - -HRESULT CRenderEngine::EndDraw() { - return m_d2dContext->EndDraw(); -} - -HRESULT CRenderEngine::Present() { - // Check if SwapChain exists before presenting - if (!m_swapChain) return S_OK; - - // VSync on (1 = wait for 1 vertical sync) - return m_swapChain->Present(1, 0); -} - -void CRenderEngine::DrawOSD(const OSDState& state) { - if (!state.IsVisible() || !m_d2dContext || !m_dwriteFactory || !m_textFormat) return; - - // 1. Create Text Layout - ComPtr textLayout; - HRESULT hr = m_dwriteFactory->CreateTextLayout( - state.Message.c_str(), - (UINT32)state.Message.length(), - m_textFormat.Get(), - 2000.0f, // Max width (unconstrained mostly) - 100.0f, // Max height - &textLayout - ); - if (FAILED(hr)) return; - - // 2. Measure Text - DWRITE_TEXT_METRICS metrics; - textLayout->GetMetrics(&metrics); - - // 3. Calculate Toast Geometry - float paddingH = 30.0f; - float paddingV = 15.0f; - float toastW = metrics.width + paddingH * 2; - float toastH = metrics.height + paddingV * 2; - - D2D1_SIZE_F targetSize = m_d2dContext->GetSize(); - float x = (targetSize.width - toastW) / 2.0f; - float y = targetSize.height - toastH - 100.0f; // 100px from bottom - - D2D1_RECT_F toastRect = D2D1::RectF(x, y, x + toastW, y + toastH); - D2D1_ROUNDED_RECT roundedRect = D2D1::RoundedRect(toastRect, 8.0f, 8.0f); - - // 4. Create Brushes (On-the-fly for simplicity, cached is better but this is OSD) - ComPtr bgBrush; - ComPtr textBrush; - - // Use semi-transparent black for BG - m_d2dContext->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.7f), &bgBrush); - - // Resolve Text Color - D2D1_COLOR_F finalTextColor = state.CustomColor; - if (finalTextColor.a == 0.0f) { - // Default colors - if (state.IsError) finalTextColor = D2D1::ColorF(D2D1::ColorF::Red); - else if (state.IsWarning) finalTextColor = D2D1::ColorF(D2D1::ColorF::Yellow); - else finalTextColor = D2D1::ColorF(D2D1::ColorF::White); - } - m_d2dContext->CreateSolidColorBrush(finalTextColor, &textBrush); - - if (bgBrush && textBrush) { - // 5. Draw - // We can use PushLayer here for localized effects, but FillRoundedRect is sufficient for flat transparent overlay. - // Adhering to the plan: "Use PushLayer". - // PushLayer allows us to clip rendering to the rounded rect if we were drawing something complex inside. - // For a solid fill, strict PushLayer usage is overkill but I will use it to ensure I follow "layers" promise roughly - // OR I will just use FillRoundedRect because it IS a layer of paint. - // Actually, let's stick to FillRoundedRect for the background. It blends correctly. - - m_d2dContext->FillRoundedRectangle(roundedRect, bgBrush.Get()); - - // Draw Text - // Center text in the toast - // We already aligned text center in Format, but we need to place the layout box - // We created layout with 2000 width. We should probably Draw using the Origin - D2D1_POINT_2F textOrigin = D2D1::Point2F(x + paddingH, y + paddingV); - m_d2dContext->DrawTextLayout(textOrigin, textLayout.Get(), textBrush.Get()); - } -} - -HRESULT CRenderEngine::CreateBitmapFromWIC(IWICBitmapSource* wicBitmap, ID2D1Bitmap** d2dBitmap) { - if (!wicBitmap || !d2dBitmap) return E_INVALIDARG; - - HRESULT hr = S_OK; - - // Convert pixel format - ComPtr converter; - hr = m_wicFactory->CreateFormatConverter(&converter); - if (FAILED(hr)) return hr; - - hr = converter->Initialize( - wicBitmap, - GUID_WICPixelFormat32bppPBGRA, // Premultiplied alpha for D2D - WICBitmapDitherTypeNone, - nullptr, - 0.0f, - WICBitmapPaletteTypeCustom - ); - if (FAILED(hr)) return hr; - - // Create D2D bitmap from WIC - hr = m_d2dContext->CreateBitmapFromWicBitmap( - converter.Get(), - nullptr, - d2dBitmap - ); - - return hr; -} diff --git a/QuickView/RenderEngine.h b/QuickView/RenderEngine.h deleted file mode 100644 index 25ff94a0..00000000 --- a/QuickView/RenderEngine.h +++ /dev/null @@ -1,101 +0,0 @@ -#include "pch.h" -#include -#include -#include -#include "OSDState.h" -#pragma comment(lib, "dwrite.lib") - -/// -/// Direct2D Render Engine (Simplified) -/// Focuses on accurate image rendering without color adjustments -/// Manages D3D11 device, D2D context, DXGI SwapChain (Flip Model) -/// -class CRenderEngine { -public: - CRenderEngine() = default; - ~CRenderEngine(); - - /// - /// Initialize render engine - /// - HRESULT Initialize(HWND hwnd); - - /// - /// Resize SwapChain (call on window resize) - /// - HRESULT Resize(UINT width, UINT height); - - /// - /// Begin frame drawing - /// - void BeginDraw(); - - /// - /// Clear background color - /// - void Clear(const D2D1_COLOR_F& color); - - /// - /// Draw bitmap to specified rectangle with high quality interpolation - /// - void DrawBitmap(ID2D1Bitmap* bitmap, const D2D1_RECT_F& destRect); - - /// - /// End drawing and commit - /// - HRESULT EndDraw(); - - /// - /// Present frame (Flip Model zero-copy) - /// - HRESULT Present(); - - /// - /// Create D2D bitmap from WIC bitmap - /// - HRESULT CreateBitmapFromWIC(IWICBitmapSource* wicBitmap, ID2D1Bitmap** d2dBitmap); - - /// - /// Get D2D device context - /// - ID2D1DeviceContext* GetDeviceContext() const { return m_d2dContext.Get(); } - - /// - /// Get WIC factory - /// - IWICImagingFactory* GetWICFactory() const { return m_wicFactory.Get(); } - -private: - HRESULT CreateDeviceResources(); - HRESULT CreateSwapChain(HWND hwnd); - HRESULT CreateRenderTarget(); - - HWND m_hwnd = nullptr; - - // D3D11 resources - ComPtr m_d3dDevice; - ComPtr m_d3dContext; - - // DXGI resources - ComPtr m_swapChain; - HANDLE m_frameLatencyWaitableObject = nullptr; - -public: - void WaitForGPU(); - bool IsWaitable() const { return m_frameLatencyWaitableObject != nullptr; } - void DrawOSD(const OSDState& state); - - - // D2D resources - ComPtr m_d2dFactory; - ComPtr m_d2dDevice; - ComPtr m_d2dContext; - ComPtr m_targetBitmap; - - // DirectWrite resources - ComPtr m_dwriteFactory; - ComPtr m_textFormat; - - // WIC resources - ComPtr m_wicFactory; -}; diff --git a/QuickView/StbLoader.cpp b/QuickView/StbLoader.cpp deleted file mode 100644 index 8d7c762b..00000000 --- a/QuickView/StbLoader.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "pch.h" -#include "StbLoader.h" - -// Define implementation for stb_image -#define STB_IMAGE_IMPLEMENTATION -#define STBI_NO_JPEG // Use WIC or LibJPEG-Turbo -#define STBI_NO_PNG // Use Wuffs -#define STBI_NO_GIF // Use Wuffs -#define STBI_NO_BMP // Use Wuffs -#define STBI_NO_TGA // Use Wuffs (saves ~3KB) -// Keep PSD, HDR, PIC, PNM (stb handles these, Wuffs doesn't fully) - -#include "../third_party/stb/stb_image.h" -#include - -// ---------------------------------------------------------------------------- -// Zlib Shim for TinyEXR (using System Zlib / zlib-ng) -// ---------------------------------------------------------------------------- -extern "C" int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) { - uLongf destLen = (uLongf)olen; - int ret = uncompress((Bytef*)obuffer, &destLen, (const Bytef*)ibuffer, (uLong)ilen); - if (ret == Z_OK) return (int)destLen; - return -1; -} - -extern "C" unsigned char *stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) { - uLongf destLen = compressBound(data_len); - unsigned char* dest = (unsigned char*)malloc(destLen); - if (!dest) return nullptr; - if (compress(dest, &destLen, data, data_len) != Z_OK) { - free(dest); - return nullptr; - } - if (out_len) *out_len = (int)destLen; - return dest; -} - -namespace StbLoader { - - bool LoadImage(const char* filename, - int* width, int* height, int* channels, - std::vector& outData, bool useFloat) { - - // stbi_load returns data that needs to be freed - // We want 4 channels (RGBA) forced for simplified handling - int w, h, c; - void* data = nullptr; - - if (useFloat) { - data = stbi_loadf(filename, &w, &h, &c, 4); - } else { - data = stbi_load(filename, &w, &h, &c, 4); - } - - if (!data) return false; - - *width = w; - *height = h; - *channels = 4; - - size_t pixelSize = useFloat ? sizeof(float) : sizeof(uint8_t); - size_t totalSize = w * h * 4 * pixelSize; - - outData.resize(totalSize); - memcpy(outData.data(), data, totalSize); - - stbi_image_free(data); - return true; - } - - bool LoadImageFromMemory(const uint8_t* inData, size_t size, - int* width, int* height, int* channels, - std::vector& outData, bool useFloat) { - - int w, h, c; - void* data = nullptr; - - if (useFloat) { - data = stbi_loadf_from_memory(inData, (int)size, &w, &h, &c, 4); // Force 4 channels - } else { - data = stbi_load_from_memory(inData, (int)size, &w, &h, &c, 4); // Force 4 channels - } - - if (!data) return false; - - *width = w; - *height = h; - *channels = 4; - - size_t pixelSize = useFloat ? sizeof(float) : sizeof(uint8_t); - size_t totalSize = w * h * 4 * pixelSize; - - outData.resize(totalSize); - memcpy(outData.data(), data, totalSize); - - stbi_image_free(data); - return true; - } - - int ZlibDecode(char* obuffer, int olen, const char* ibuffer, int ilen) { - // Use system zlib - uLongf destLen = (uLongf)olen; - int ret = uncompress((Bytef*)obuffer, &destLen, (const Bytef*)ibuffer, (uLong)ilen); - if (ret == Z_OK) return (int)destLen; - return -1; - } - - bool IsSupported(const char* filename) { - // stbi_info checks magic - int w, h, c; - return stbi_info(filename, &w, &h, &c) != 0; - } -} diff --git a/QuickView/StbLoader.h b/QuickView/StbLoader.h deleted file mode 100644 index d7d0106d..00000000 --- a/QuickView/StbLoader.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include -#include -#include - -namespace StbLoader { - - /// - /// Load image using stb_image.h - /// Supports: PSD (Composite), HDR, PIC, PNM, TGA (Fallback) - /// - bool LoadImage(const char* filename, - int* width, int* height, int* channels, - std::vector& outData, bool useFloat); - - bool LoadImageFromMemory(const uint8_t* inData, size_t size, - int* width, int* height, int* channels, - std::vector& outData, bool useFloat); - - // Expose internal zlib decoder for TinyEXR - int ZlibDecode(char* obuffer, int olen, const char* ibuffer, int ilen); - - /// - /// Check if format is supported by checking file signature - /// - bool IsSupported(const char* filename); - -} diff --git a/QuickView/ThumbnailManager.cpp b/QuickView/ThumbnailManager.cpp deleted file mode 100644 index 7f6ea6b8..00000000 --- a/QuickView/ThumbnailManager.cpp +++ /dev/null @@ -1,332 +0,0 @@ -#include "pch.h" -#include "ThumbnailManager.h" -#include -#include - -ThumbnailManager::ThumbnailManager() {} - -ThumbnailManager::~ThumbnailManager() { - Shutdown(); -} - -void ThumbnailManager::Initialize(HWND hwnd, CImageLoader* pLoader) { - m_hwnd = hwnd; - m_pLoader = pLoader; - m_running = true; - m_workerThreadFast = std::thread(&ThumbnailManager::WorkerLoopFast, this); - m_workerThreadSlow = std::thread(&ThumbnailManager::WorkerLoopSlow, this); -} - -void ThumbnailManager::Shutdown() { - m_running = false; - m_cvFast.notify_all(); - m_cvSlow.notify_all(); - if (m_workerThreadFast.joinable()) m_workerThreadFast.join(); - if (m_workerThreadSlow.joinable()) m_workerThreadSlow.join(); - ClearCache(); -} - -void ThumbnailManager::ClearCache() { - std::lock_guard lock(m_cacheMutex); - m_l1Cache.clear(); - m_l2Cache.clear(); - m_lruList.clear(); - m_lruMap.clear(); - m_currentCacheSize = 0; - - // Also clear queue? - std::lock_guard queueLock(m_queueMutex); - m_pendingTasks.clear(); - m_fastQueue = std::priority_queue, std::greater>(); - m_slowQueue = std::priority_queue, std::greater>(); -} - -ComPtr ThumbnailManager::GetThumbnail(int fileIndex, LPCWSTR filePath, ID2D1RenderTarget* pRT) { - std::lock_guard lock(m_cacheMutex); - - // 1. Check L2 Cache (GPU Bitmap) - auto itL2 = m_l2Cache.find(fileIndex); - if (itL2 != m_l2Cache.end()) { - TouchLRU(fileIndex); - return itL2->second; - } - - // 2. Check L1 Cache (Raw Data) - Promote to L2 - auto itL1 = m_l1Cache.find(fileIndex); - if (itL1 != m_l1Cache.end()) { - // Upload to GPU - ComPtr bmp; - if (pRT && !itL1->second.pixels.empty()) { - D2D1_BITMAP_PROPERTIES props = D2D1::BitmapProperties( - D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED) - ); - D2D1_SIZE_U size = D2D1::SizeU(itL1->second.width, itL1->second.height); - - pRT->CreateBitmap(size, itL1->second.pixels.data(), itL1->second.stride, &props, &bmp); - - if (bmp) { - // Determine size for LRU (approx VRAM usage) - size_t sizeBytes = itL1->second.pixels.size(); - - // Store in L2 - m_l2Cache[fileIndex] = bmp; - - // Remove from L1 (Save RAM, assuming we don't need to rebuild often without reloading) - // Actually user said: "Keep L1 Cache... allows rebuilding texture quickly". - // If we keep L1, we double memory usage (RAM + VRAM). - // But user explicitly requested "Dual-Layer Cache" and "L1 Cache... allows rebuilding...". - // So we KEEP it in L1? - // "L1 Cache (RAM): ... Stores raw decoded pixels." - // "L2 Cache (VRAM): ... Stores uploaded GPU textures." - // "Cache structure: ... L1 Cache ... L2 Cache." - // "Retain L1 instructions? -> Yes, retain L1 for device lost recovery." - // Okay, we keep BOTH. - // sizeBytes is already accounted for when added to L1. - - // L2 addition doesn't change RAM usage (managed by driver usually), but we track VRAM loosely? - // Strict LRU limit says 200MB. Is that RAM or VRAM? - // Usually RAM. L1 is RAM. L2 is VRAM. - // Let's count L1 size towards limit. - } - } - TouchLRU(fileIndex); - return bmp; - } - - // 3. Not in Cache - Queue it (if not already) - // We don't queue here directly to avoid spamming lock. - // Usually Overlay calls UpdateOptimizedPriority to manage queue. - // But if we just need one specific file (e.g. current center), we could force queue it? - // Let's rely on UpdateOptimizedPriority for batch queuing. - // Return null for now. - return nullptr; -} - -void ThumbnailManager::UpdateOptimizedPriority(int startIdx, int endIdx, int priorityCenter) { - std::lock_guard lock(m_queueMutex); - - // Strategy: Clear queue to remove old far-away tasks? - // User requested: "Cancellation: If user scrolls fast... discard previous tasks". - // So yes, clearing is good. - // But don't clear tasks that are *inside* the new range? - // Just clear everything and re-add the new range. The overlap is fine to re-add (deduplication via m_pendingTasks needs check). - - // Note: clearing m_taskQueue is hard (no clear()). Assign new. - m_fastQueue = std::priority_queue, std::greater>(); - m_slowQueue = std::priority_queue, std::greater>(); - - // Also clear pending flags? - // If we clear queue, those tasks are gone. So we should clear pending flags for them. - // But we don't know which ones were in queue. - // Simpler: Clear m_pendingTasks map too. - // But wait, what if Worker is currently processing index X? - // Worker holds no lock during decode. - // If we clear pending map, and add X again, we might double process X. - // That's acceptable (waste of one decode) for simplicity. - m_pendingTasks.clear(); - - // Add new detailed range - // Expand a bit? Start-5 to End+5 ? - int range = 10; - int realStart = std::max(0, startIdx - range); - int realEnd = endIdx + range; // Need max count check? Caller usually handles indices? - // We don't know Max count here unless passed. Assuming caller passes valid indices or we check inside loop? - // We'll trust caller (GalleryOverlay) to pass reasonable indices or we check against FileNavigator if we had access. - // We bind path in Task, so we need Path access. - // Wait, GetThumbnail passed Path. But UpdateOptimizedPriority doesn't. - // We need access to FileNavigator to get Path by Index! - // We don't have FileNavigator here directly. - // Solution: GetThumbnail calls are driven by Overlay iterating FileNavigator. - // But `UpdateOptimizedPriority` is the "Bulk Queue" function. - // We should pass a callback or have reference to FileNavigator? - // Or `GetThumbnail` queues it if missing? - - // Revised Plan Logic: - // GalleryOverlay calculates range. - // GalleryOverlay iterates range, calls `ThumbnailManager::EnsureQueued(index, path, priority)`. - // Creating `EnsureQueued` helper. -} - -// Helper (Internal or Public?) - Let's make it Public for Overlay to use iteratively -// Actually, let's change UpdateOptimizedPriority to accept a list of needed thumbs? -// Or just let Overlay loop and call "QueueIfNotCached". -// Let's add `QueueRequest(int index, LPCWSTR path, int priority)` to public API. -// And `ClearQueue()` for the "Fast Scroll" cancellation. - -void ThumbnailManager::EvictLRU() { - // Must be called with m_cacheMutex locked - while (m_currentCacheSize > MAX_CACHE_SIZE || m_l1Cache.size() > MAX_CACHE_COUNT) { - if (m_lruList.empty()) break; - - int idxToRemove = m_lruList.back(); - - // Remove from maps - auto itL1 = m_l1Cache.find(idxToRemove); - if (itL1 != m_l1Cache.end()) { - m_currentCacheSize -= itL1->second.pixels.size(); - m_l1Cache.erase(itL1); - } - m_l2Cache.erase(idxToRemove); - - // Remove from LRU - m_lruMap.erase(idxToRemove); - m_lruList.pop_back(); - } -} - -void ThumbnailManager::AddToLRU(int index, size_t size) { - // Remove if exists (re-add to front) - if (m_lruMap.count(index)) { - m_lruList.erase(m_lruMap[index]); - m_currentCacheSize -= size; // Size might satisfy if unchanged? Simpler to re-add. - // Actually size is stored in entry. - } - - m_lruList.push_front(index); - m_lruMap[index] = m_lruList.begin(); - m_currentCacheSize += size; - - EvictLRU(); -} - -void ThumbnailManager::TouchLRU(int index) { - if (m_lruMap.count(index)) { - m_lruList.splice(m_lruList.begin(), m_lruList, m_lruMap[index]); - m_lruMap[index] = m_lruList.begin(); - } -} - -void ThumbnailManager::WorkerLoopFast() { - while (m_running) { - Task task; - { - std::unique_lock lock(m_queueMutex); - m_cvFast.wait(lock, [this] { return !m_fastQueue.empty() || !m_running; }); - - if (!m_running) break; - - // Double check empty in case of spurious wake or stolen task (unlikely with one consumer per queue, but safe) - if (m_fastQueue.empty()) continue; - - task = m_fastQueue.top(); - m_fastQueue.pop(); - - auto it = m_pendingTasks.find(task.index); - if (it != m_pendingTasks.end()) m_pendingTasks.erase(it); - } - - int targetSize = 300; - CImageLoader::ThumbData data; - if (SUCCEEDED(m_pLoader->LoadThumbnail(task.path.c_str(), targetSize, &data)) && data.isValid) { - { - std::lock_guard lock(m_cacheMutex); - size_t size = data.pixels.size(); - m_l1Cache[task.index] = std::move(data); - AddToLRU(task.index, size); - } - PostMessage(m_hwnd, WM_THUMB_KEY_READY, (WPARAM)task.index, 0); - } - } -} - -void ThumbnailManager::WorkerLoopSlow() { - while (m_running) { - Task task; - { - std::unique_lock lock(m_queueMutex); - m_cvSlow.wait(lock, [this] { return !m_slowQueue.empty() || !m_running; }); - - if (!m_running) break; - - if (m_slowQueue.empty()) continue; - - task = m_slowQueue.top(); - m_slowQueue.pop(); - - auto it = m_pendingTasks.find(task.index); - if (it != m_pendingTasks.end()) m_pendingTasks.erase(it); - } - - int targetSize = 300; - CImageLoader::ThumbData data; - if (SUCCEEDED(m_pLoader->LoadThumbnail(task.path.c_str(), targetSize, &data)) && data.isValid) { - { - std::lock_guard lock(m_cacheMutex); - size_t size = data.pixels.size(); - m_l1Cache[task.index] = std::move(data); - AddToLRU(task.index, size); - } - PostMessage(m_hwnd, WM_THUMB_KEY_READY, (WPARAM)task.index, 0); - } - } -} - -// Added to match planned API changes -void ThumbnailManager::QueueRequest(int index, LPCWSTR path, int priority) { - std::lock_guard lock(m_queueMutex); - - if (m_pendingTasks.count(index)) return; // Already queued - - // Check if in Cache? (Avoid queuing if already cached) - // Accessing m_cacheMutex here might deadlock if called from UI thread which holds cacheMutex? - // Caller (UI) usually checks GetThumbnail (which locks Cache) -> returns null -> calls QueueRequest. - // So Cache is unlocked when QueueRequest is called. Safe. - // But we should double check L1 existence? - // Assuming UI checked GetThumbnail first. - - Task t; - t.index = index; - t.path = path; - t.priorityDistance = priority; - - // Detect format for Lane Selection - std::wstring ext = path; - size_t dot = ext.find_last_of(L'.'); - bool isFast = false; - if (dot != std::wstring::npos) { - std::wstring e = ext.substr(dot); - std::transform(e.begin(), e.end(), e.begin(), ::towlower); - // Fast Lane: JPEG (Optimized) + RAW/HEIC/PSD (Embedded Preview) + WebP (Scaled) - if (e == L".jpg" || e == L".jpeg" || e == L".jpe" || e == L".jfif" || - e == L".arw" || e == L".cr2" || e == L".nef" || e == L".dng" || e == L".orf" || e == L".rw2" || e == L".raf" || - e == L".heic" || e == L".heif" || e == L".avif" || - e == L".psd" || e == L".psb" || - e == L".webp") { // Debug Stats (Removed) - isFast = true; - } - } - t.isFastLane = isFast; - - if (isFast) { - m_fastQueue.push(t); - m_cvFast.notify_one(); - } else { - m_slowQueue.push(t); - m_cvSlow.notify_one(); - } - - m_pendingTasks[index] = true; -} - -void ThumbnailManager::ClearQueue() { - std::lock_guard lock(m_queueMutex); - m_fastQueue = std::priority_queue, std::greater>(); - m_slowQueue = std::priority_queue, std::greater>(); - m_pendingTasks.clear(); -} - - - -ThumbnailManager::ImageInfo ThumbnailManager::GetImageInfo(int index) { - std::lock_guard lock(m_cacheMutex); - auto it = m_l1Cache.find(index); - if (it != m_l1Cache.end()) { - ImageInfo info; - info.origWidth = it->second.origWidth; - info.origHeight = it->second.origHeight; - info.fileSize = it->second.fileSize; - info.isValid = true; - return info; - } - return { 0, 0, 0, false }; -} diff --git a/QuickView/ThumbnailManager.h b/QuickView/ThumbnailManager.h deleted file mode 100644 index 0ad19dfc..00000000 --- a/QuickView/ThumbnailManager.h +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once -#include "pch.h" -#include "ImageLoader.h" -#include -#include -#include -#include -#include -#include -#include - -#define WM_THUMB_KEY_READY (WM_USER + 101) - -class ThumbnailManager { -public: - ThumbnailManager(); - ~ThumbnailManager(); - - // Singleton-like access if needed, or passed as dependency. - // For now, let's assume it's a member of MainWindow. - - // Debug Stats (Removed) - - - struct ImageInfo { - int origWidth; - int origHeight; - uint64_t fileSize; - bool isValid; - }; - ImageInfo GetImageInfo(int index); - - void Initialize(HWND hwnd, CImageLoader* pLoader); - void Shutdown(); - - // The main API called by UI (GalleryOverlay) - // Returns the bitmap if ready (L2 Cache). - // If not ready, queues a request and returns nullptr. - // If L1 Cache hit (Raw Data), immediately creates Bitmap (L2) and returns it. - ComPtr GetThumbnail(int fileIndex, LPCWSTR filePath, ID2D1RenderTarget* pRT); - - // Call this when file list changes completely - void ClearCache(); - - // Preload range (Virtualization hints) - // priorityCenter: index that viewer is looking at - void UpdateOptimizedPriority(int startIdx, int endIdx, int priorityCenter); - - // Dynamic queue management - void QueueRequest(int index, LPCWSTR path, int priority); - void ClearQueue(); - -private: - struct CacheEntry { - CImageLoader::ThumbData rawData; // L1 - ComPtr bitmap; // L2 - size_t sizeBytes = 0; - }; - - HWND m_hwnd = nullptr; - CImageLoader* m_pLoader = nullptr; - - // --- Cache Storage --- - // Combined L1+L2 entry for simplicity, but strictly managed thread access - // L1 part accessed by Worker (Write) and UI (Read/Consume) - // L2 part accessed by UI only - // Wait, separate maps might be cleaner for locking? - // Or just one map protected by mutex? - // User requested L1/L2 distinction. - // L1: Raw DataMap. L2: BitmapMap. - // Index -> RawData - // Index -> Bitmap - // Actually, locking individual entries is complex. - // Let's use a single mutex for the cache structure. - - std::mutex m_cacheMutex; - std::unordered_map m_l1Cache; // Worker writes, UI reads - std::unordered_map> m_l2Cache; // UI only (but we track size here) - - // LRU Tracking - std::list m_lruList; - std::unordered_map::iterator> m_lruMap; - size_t m_currentCacheSize = 0; - - // Constants - const size_t MAX_CACHE_SIZE = 200 * 1024 * 1024; // 200MB - const size_t MAX_CACHE_COUNT = 1000; - - // --- Worker Thread --- - // --- Worker Threads --- - struct Task { - int index; - std::wstring path; - int priorityDistance; // 0 = highest (center) - bool isFastLane; // Tag to verify lane if needed - - bool operator>(const Task& other) const { - return priorityDistance > other.priorityDistance; // Min-heap - } - }; - - std::thread m_workerThreadFast; - std::thread m_workerThreadSlow; - - std::mutex m_queueMutex; // Protects BOTH queues and pending map - std::condition_variable m_cvFast; - std::condition_variable m_cvSlow; - - std::priority_queue, std::greater> m_fastQueue; - std::priority_queue, std::greater> m_slowQueue; - - std::unordered_map m_pendingTasks; - std::atomic m_running = false; - - void WorkerLoopFast(); - void WorkerLoopSlow(); - - void EvictLRU(); - void AddToLRU(int index, size_t size); - void TouchLRU(int index); -}; diff --git a/QuickView/TinyExrLoader.cpp b/QuickView/TinyExrLoader.cpp deleted file mode 100644 index ee5b11ce..00000000 --- a/QuickView/TinyExrLoader.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "pch.h" -#include "TinyExrLoader.h" -#include -#include "StbLoader.h" // For ZlibDecode - -// Define implementation -// Define implementation -// Use system zlib (zlib-ng) for performance -#define TINYEXR_USE_MINIZ 0 -#undef TINYEXR_USE_STB_ZLIB -#define TINYEXR_USE_STB_ZLIB 1 -#include - -// Wrappers moved to StbLoader.cpp to solve linkage -// Forward decl if needed, but TinyEXR header declares it. - -#define TINYEXR_IMPLEMENTATION -#include "../third_party/tinyexr/tinyexr.h" - -namespace TinyExrLoader { - - bool LoadEXR(const char* filename, - int* width, int* height, - std::vector& outData) { - - float* out = nullptr; // width * height * RGBA - int w = 0, h = 0; - const char* err = nullptr; - - int ret = ::LoadEXR(&out, &w, &h, filename, &err); - - if (ret != TINYEXR_SUCCESS) { - if (err) { - // Log? - ::FreeEXRErrorMessage(err); // Must free err - } - return false; - } - - *width = w; - *height = h; - - // Copy to vector - size_t size = (size_t)w * h * 4; - outData.resize(size); - // tinyexr outputs float per component. - memcpy(outData.data(), out, size * sizeof(float)); - - free(out); // tinyexr uses free - return true; - } - - bool LoadEXRFromMemory(const uint8_t* inData, size_t size, - int* width, int* height, - std::vector& outData) { - float* out = nullptr; - int w = 0, h = 0; - const char* err = nullptr; - - // Use global LoadEXRFromMemory - int ret = ::LoadEXRFromMemory(&out, &w, &h, inData, size, &err); - - if (ret != TINYEXR_SUCCESS) { - if (err) { - OutputDebugStringA("TinyEXR Error: "); - OutputDebugStringA(err); - OutputDebugStringA("\n"); - MessageBoxA(nullptr, err, "TinyEXR Error", MB_OK | MB_ICONERROR); - ::FreeEXRErrorMessage(err); - } else { - OutputDebugStringA("TinyEXR Error: Unknown error (ret code check needed)\n"); - } - return false; - } - - *width = w; - *height = h; - - size_t pixelSize = (size_t)w * h * 4; - outData.resize(pixelSize); - memcpy(outData.data(), out, pixelSize * sizeof(float)); - - free(out); - return true; - } -} diff --git a/QuickView/TinyExrLoader.h b/QuickView/TinyExrLoader.h deleted file mode 100644 index 934918cc..00000000 --- a/QuickView/TinyExrLoader.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include -#include - -namespace TinyExrLoader { - - /// - /// Load EXR image - /// Returns RGBA float data - /// - bool LoadEXR(const char* filename, - int* width, int* height, - std::vector& outData); - - bool LoadEXRFromMemory(const uint8_t* inData, size_t size, - int* width, int* height, - std::vector& outData); -} diff --git a/QuickView/Toolbar.cpp b/QuickView/Toolbar.cpp deleted file mode 100644 index 17e2b6bf..00000000 --- a/QuickView/Toolbar.cpp +++ /dev/null @@ -1,332 +0,0 @@ -#include "pch.h" -#include "Toolbar.h" - -// Icon Codes (Segoe Fluent Icons) -#define ICON_PREV L"\uE76B" -#define ICON_NEXT L"\uE76C" -#define ICON_ROTATE_L L"\uE7AD" -#define ICON_ROTATE_R L"\uE7AD" -#define ICON_FLIP L"\uE8AB" // Mirror -#define ICON_LOCK L"\uE9A6" // Lock icon (same for both states, color changes) -#define ICON_UNLOCK L"\uE9A6" -#define ICON_GALLERY L"\uE80A" -#define ICON_INFO L"\uE946" -#define ICON_RAW L"\uE722" // RAW icon (same for both states, color changes) -#define ICON_WARNING L"\uE7BA" -#define ICON_PIN L"\uE718" -#define ICON_UNPIN L"\uE77A" - -Toolbar::Toolbar() { - // Define Buttons - m_buttons = { - { ToolbarButtonID::Prev, ICON_PREV[0], L"Previous (Left)", {}, true }, - { ToolbarButtonID::Next, ICON_NEXT[0], L"Next (Right)", {}, true }, - // Spacer? Just gap. - { ToolbarButtonID::RotateL, ICON_ROTATE_L[0], L"Rotate Left (Shift+R)", {}, true }, - { ToolbarButtonID::RotateR, ICON_ROTATE_R[0], L"Rotate Right (R)", {}, true }, - { ToolbarButtonID::FlipH, ICON_FLIP[0], L"Flip Horizontal (H)", {}, true }, - - { ToolbarButtonID::LockSize, ICON_LOCK[0], L"Lock Window Size", {}, true, false }, - { ToolbarButtonID::Gallery, ICON_GALLERY[0], L"Gallery (T)", {}, true }, - - { ToolbarButtonID::Exif, ICON_INFO[0], L"Info Panel", {}, true, false }, - { ToolbarButtonID::RawToggle, ICON_RAW[0], L"RAW Preview (Fast)", {}, false, false }, // Hidden/Disabled if not RAW - { ToolbarButtonID::FixExtension, ICON_WARNING[0], L"Extension Mismatch (Fix)", {}, false, false, true }, // Hidden if no mismatch - - { ToolbarButtonID::Pin, ICON_PIN[0], L"Pin Toolbar", {}, true, false } - }; -} - -Toolbar::~Toolbar() {} - -void Toolbar::CreateResources(ID2D1RenderTarget* pRT) { - if (!m_brushBg) { - pRT->CreateSolidColorBrush(D2D1::ColorF(0.1f, 0.1f, 0.1f, 0.85f), &m_brushBg); // Dark background - pRT->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), &m_brushIcon); - pRT->CreateSolidColorBrush(D2D1::ColorF(0.4f, 0.6f, 1.0f, 1.0f), &m_brushIconActive); // Blue for active - pRT->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.3f), &m_brushIconDisabled); - pRT->CreateSolidColorBrush(D2D1::ColorF(1.0f, 0.3f, 0.3f, 1.0f), &m_brushWarning); // Red for warning - pRT->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.1f), &m_brushHover); // Hover highlight - - // Font - DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(m_dwriteFactory.GetAddressOf())); - if (m_dwriteFactory) { - m_dwriteFactory->CreateTextFormat( - L"Segoe Fluent Icons", - NULL, - DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 20.0f, // Icon Size - L"en-us", - &m_textFormatIcon - ); - // Fallback for Win10? "Segoe MDL2 Assets" - // If Segoe Fluent Icons not found, it shows box. - // We can try to detect or just use MDL2 if simpler? - // Users requested Fluent. Keep Fluent. - if (m_textFormatIcon) { - m_textFormatIcon->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); - m_textFormatIcon->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); - } - } - } -} - -void Toolbar::Init(ID2D1RenderTarget* pRT) { - CreateResources(pRT); -} - -void Toolbar::UpdateLayout(float winW, float winH) { - - // Simpler: Count visible buttons - int visibleCount = 0; - for (const auto& btn : m_buttons) { - bool visible = true; - if (btn.id == ToolbarButtonID::RawToggle && !btn.isEnabled) visible = false; - if (btn.id == ToolbarButtonID::FixExtension && !btn.isWarning) visible = false; - if (visible) visibleCount++; - } - - // Calculate total width: padding + buttons + gaps between buttons - float totalW = PADDING_X * 2 + (visibleCount * BUTTON_SIZE); - if (visibleCount > 1) totalW += (visibleCount - 1) * GAP; - - float startX = (winW - totalW) / 2.0f; - float startY = winH - BOTTOM_MARGIN - BUTTON_SIZE - PADDING_Y * 2; - - m_bgRect = D2D1::RoundedRect( - D2D1::RectF(startX, startY, startX + totalW, startY + BUTTON_SIZE + PADDING_Y * 2), - 20.0f, 20.0f // Capsule radius - ); - - // Layout Buttons - float cx = startX + PADDING_X; - float cy = startY + PADDING_Y; - - for (auto& btn : m_buttons) { - bool visible = true; - if (btn.id == ToolbarButtonID::RawToggle && !btn.isEnabled) visible = false; - if (btn.id == ToolbarButtonID::FixExtension && !btn.isWarning) visible = false; - - // Sync Pin State - if (btn.id == ToolbarButtonID::Pin) { - btn.isToggled = m_isPinned; - btn.iconChar = m_isPinned ? ICON_UNPIN[0] : ICON_PIN[0]; - btn.tooltip = m_isPinned ? L"Unpin Toolbar" : L"Pin Toolbar"; - } - - if (visible) { - btn.rect = D2D1::RectF(cx, cy, cx + BUTTON_SIZE, cy + BUTTON_SIZE); - cx += BUTTON_SIZE + GAP; - } else { - btn.rect = D2D1::RectF(0,0,0,0); // Hide - } - } -} - -void Toolbar::Render(ID2D1RenderTarget* pRT) { - if (m_opacity <= 0.0f) return; - - CreateResources(pRT); // Ensure resources - - // Set global opacity - // Cannot set on RenderTarget easily without Layer. - // Use Layer or set Brush opacity. - // Layer is cleaner for semi-transparent group. - - ComPtr layer; - if (SUCCEEDED(pRT->CreateLayer(&layer))) { - D2D1_LAYER_PARAMETERS params = D2D1::LayerParameters(); - params.contentBounds = m_bgRect.rect; - params.opacity = m_opacity; - - pRT->PushLayer(params, layer.Get()); - - // Background - m_brushBg->SetOpacity(0.85f); // Base opacity - pRT->FillRoundedRectangle(m_bgRect, m_brushBg.Get()); - - // Buttons - for (const auto& btn : m_buttons) { - if (btn.rect.right == 0) continue; // Hidden - - // Hover effect with rounded corners - if (btn.isHovered) { - D2D1_ROUNDED_RECT hoverRect = D2D1::RoundedRect(btn.rect, 6.0f, 6.0f); - pRT->FillRoundedRectangle(hoverRect, m_brushHover.Get()); - } - - // Icon Brush - ID2D1SolidColorBrush* pBrush = m_brushIcon.Get(); - if (btn.isToggled) pBrush = m_brushIconActive.Get(); - if (btn.isWarning) pBrush = m_brushWarning.Get(); - if (btn.id == ToolbarButtonID::LockSize && btn.isToggled) pBrush = m_brushIconActive.Get(); - if (btn.id == ToolbarButtonID::Pin && btn.isToggled) pBrush = m_brushIconActive.Get(); - - // Specific Icon Logic - wchar_t icon = btn.iconChar; - - // Rotate Mirroring check - if (btn.id == ToolbarButtonID::RotateL) { - // Manual mirror? Or just render. Rotate icon usually implies CW. - // For CCW, we can flip using matrix or just accept it. - // Let's use matrix to flip horizontally for RotateL if needed. - // Center of button - // D2D1::Matrix3x2F::Scale(-1.0f, 1.0f, center) - float cx = (btn.rect.left + btn.rect.right)/2; - float cy = (btn.rect.top + btn.rect.bottom)/2; - pRT->SetTransform(D2D1::Matrix3x2F::Scale(-1.0f, 1.0f, D2D1::Point2F(cx, cy))); - pRT->DrawText(&icon, 1, m_textFormatIcon.Get(), btn.rect, pBrush); - pRT->SetTransform(D2D1::Matrix3x2F::Identity()); - continue; - } - - pRT->DrawText(&icon, 1, m_textFormatIcon.Get(), btn.rect, pBrush); - } - - pRT->PopLayer(); - } - - // Tooltip for hovered button (rendered OUTSIDE layer for full opacity) - for (const auto& btn : m_buttons) { - if (btn.isHovered && !btn.tooltip.empty()) { - static ComPtr tooltipFormat; - if (!tooltipFormat && m_dwriteFactory) { - m_dwriteFactory->CreateTextFormat( - L"Segoe UI", NULL, - DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, - 12.0f, L"en-us", &tooltipFormat); - if (tooltipFormat) { - tooltipFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); - tooltipFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); - } - } - - if (tooltipFormat) { - float tipWidth = btn.tooltip.length() * 7.0f + 16.0f; - float tipHeight = 22.0f; - float tipX = (btn.rect.left + btn.rect.right) / 2 - tipWidth / 2; - float tipY = m_bgRect.rect.top - tipHeight - 8.0f; - if (tipX < 5) tipX = 5; - - D2D1_RECT_F tipRect = D2D1::RectF(tipX, tipY, tipX + tipWidth, tipY + tipHeight); - - ComPtr tipBg; - pRT->CreateSolidColorBrush(D2D1::ColorF(0.15f, 0.15f, 0.15f, 0.95f), &tipBg); - pRT->FillRoundedRectangle(D2D1::RoundedRect(tipRect, 4.0f, 4.0f), tipBg.Get()); - pRT->DrawText(btn.tooltip.c_str(), (UINT32)btn.tooltip.length(), tooltipFormat.Get(), tipRect, m_brushIcon.Get()); - } - break; - } - } -} - -void Toolbar::OnMouseMove(float x, float y) { - // Check if near bottom - // We don't have window height here unless passed or stored. - // UpdateLayout stores rect. - // Trigger zone is usually managed by MainWindow, calling SetVisible. - // But button hover needs x/y. - - bool changed = false; - for (auto& btn : m_buttons) { - bool wasHovered = btn.isHovered; - btn.isHovered = false; - if (btn.rect.right > 0) { // Visible - if (x >= btn.rect.left && x < btn.rect.right && y >= btn.rect.top && y < btn.rect.bottom) { - btn.isHovered = true; - } - } - if (btn.isHovered != wasHovered) changed = true; - } - // Return changed? -} - -bool Toolbar::OnClick(float x, float y, ToolbarButtonID& outId) { - if (!IsVisible()) return false; - - // Check background hit - if (HitTest(x, y)) { - - // Check buttons - for (auto& btn : m_buttons) { - if (btn.rect.right > 0) { - if (x >= btn.rect.left && x < btn.rect.right && y >= btn.rect.top && y < btn.rect.bottom) { - outId = btn.id; - return true; - } - } - } - return true; // Consumed click on toolbar background - } - return false; -} - -bool Toolbar::HitTest(float x, float y) { - if (!IsVisible()) return false; - return (x >= m_bgRect.rect.left && x <= m_bgRect.rect.right && - y >= m_bgRect.rect.top && y <= m_bgRect.rect.bottom); -} - -void Toolbar::SetVisible(bool visible) { - m_targetVisible = visible; -} - -bool Toolbar::UpdateAnimation() { - float speed = 0.1f; - if (m_targetVisible) { - if (m_opacity < 1.0f) { - m_opacity += speed; - if (m_opacity > 1.0f) m_opacity = 1.0f; - return true; - } - } else { - if (m_opacity > 0.0f) { - m_opacity -= speed; - if (m_opacity < 0.0f) m_opacity = 0.0f; - return true; - } - } - return false; -} - -void Toolbar::SetLockState(bool locked) { - for (auto& btn : m_buttons) { - if (btn.id == ToolbarButtonID::LockSize) { - btn.isToggled = locked; - // Icon stays the same (E9A6), only color changes via isToggled - btn.tooltip = locked ? L"Unlock Window Size" : L"Lock Window Size"; - } - } -} - -void Toolbar::SetExifState(bool open) { - for (auto& btn : m_buttons) { - if (btn.id == ToolbarButtonID::Exif) { - btn.isToggled = open; - } - } -} - -void Toolbar::SetRawState(bool isRaw, bool isFullDecode) { - for (auto& btn : m_buttons) { - if (btn.id == ToolbarButtonID::RawToggle) { - btn.isEnabled = isRaw; - if (isRaw) { - btn.isToggled = isFullDecode; - // Icon stays the same (E722), only color changes via isToggled - btn.tooltip = isFullDecode ? L"RAW: Full Decode (Click for Preview)" : L"RAW: Preview (Click for Full)"; - } - } - } -} - -void Toolbar::SetExtensionWarning(bool hasMismatch) { - for (auto& btn : m_buttons) { - if (btn.id == ToolbarButtonID::FixExtension) { - btn.isWarning = hasMismatch; // Only visible if warning? - // UpdateLayout should handle visibility based on isWarning flag logic I wrote above. - } - } -} diff --git a/QuickView/Toolbar.h b/QuickView/Toolbar.h deleted file mode 100644 index 29fcd88b..00000000 --- a/QuickView/Toolbar.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once -#include "pch.h" -#include -#include - -// IDs for button actions -enum class ToolbarButtonID { - None, - Prev, Next, - RotateL, RotateR, FlipH, - LockSize, Gallery, - Exif, RawToggle, FixExtension, - Pin // New Pin Button -}; - -struct ToolbarButton { - ToolbarButtonID id; - wchar_t iconChar; // Unicode point for Segoe Fluent Icon - std::wstring tooltip; - D2D1_RECT_F rect; // Runtime layout rect - bool isEnabled = true; - bool isToggled = false; // For Lock/Exif/Raw - bool isWarning = false; // For FixExtension - bool isHovered = false; -}; - -class Toolbar { -public: - Toolbar(); - ~Toolbar(); - - void Init(ID2D1RenderTarget* pRT); - void UpdateLayout(float winW, float winH); - void Render(ID2D1RenderTarget* pRT); - - // Interaction - void OnMouseMove(float x, float y); - bool OnClick(float x, float y, ToolbarButtonID& outId); - bool HitTest(float x, float y); // New method to check if point is on toolbar - - bool IsVisible() const { return m_opacity > 0.0f; } - void SetVisible(bool visible); // Triggers animation logic external to this class? - // actually, we can just set a target state and let UpdateAnimation be called by Timer. - bool IsPinned() const { return m_isPinned; } - void TogglePin() { m_isPinned = !m_isPinned; } - - // Animation Step (returns true if still animating) - bool UpdateAnimation(); - - // State Setters - void SetLockState(bool locked); - void SetExifState(bool open); - void SetRawState(bool isRaw, bool isFullDecode); - void SetExtensionWarning(bool hasMismatch); - -private: - // Layout Constants - const float BUTTON_SIZE = 40.0f; - const float GAP = 8.0f; - const float PADDING_X = 12.0f; - const float PADDING_Y = 6.0f; - const float BOTTOM_MARGIN = 30.0f; // Gap from window bottom - - // Animation - float m_opacity = 0.0f; - - bool m_targetVisible = false; - bool m_isPinned = false; - - D2D1_ROUNDED_RECT m_bgRect = {}; - std::vector m_buttons; - - // Resources - ComPtr m_brushBg; - ComPtr m_brushIcon; - ComPtr m_brushIconActive; - ComPtr m_brushIconDisabled; - ComPtr m_brushWarning; - ComPtr m_brushHover; - - ComPtr m_textFormatIcon; - ComPtr m_dwriteFactory; // Need factory to create format - - void CreateResources(ID2D1RenderTarget* pRT); -}; diff --git a/QuickView/WuffsImpl.cpp b/QuickView/WuffsImpl.cpp deleted file mode 100644 index 0a65dfe0..00000000 --- a/QuickView/WuffsImpl.cpp +++ /dev/null @@ -1,383 +0,0 @@ -// ============================================================ -// WuffsImpl.cpp - Wuffs Library Implementation (Optimized Build) -// ============================================================ -// This file is the ONLY place that includes wuffs-v0.4.c -// It controls which modules are compiled to minimize binary size. - -// ============================================================ -// 1. Wuffs Module Configuration -// ============================================================ -// Tell Wuffs: I want manual control, don't give me the full buffet -#define WUFFS_CONFIG__MODULES - -// ------------------------------------------------------------ -// [Required] Base Dependencies -// ------------------------------------------------------------ -#define WUFFS_CONFIG__MODULE__BASE -#define WUFFS_CONFIG__MODULE__CRC32 -#define WUFFS_CONFIG__MODULE__ADLER32 -#define WUFFS_CONFIG__MODULE__DEFLATE -#define WUFFS_CONFIG__MODULE__ZLIB - -// ------------------------------------------------------------ -// [Core] Image Format Switches -// ------------------------------------------------------------ -#define WUFFS_CONFIG__MODULE__PNG -#define WUFFS_CONFIG__MODULE__GIF -#define WUFFS_CONFIG__MODULE__LZW // GIF depends on LZW -#define WUFFS_CONFIG__MODULE__BMP -#define WUFFS_CONFIG__MODULE__TARGA // Use TARGA for TGA -#define WUFFS_CONFIG__MODULE__WBMP -#define WUFFS_CONFIG__MODULE__NETPBM // PGM, PPM Binary -#define WUFFS_CONFIG__MODULE__QOI // Quite OK Image (New format) - -// ------------------------------------------------------------ -// [Optimization] Static Functions -// ------------------------------------------------------------ -#define WUFFS_CONFIG__STATIC_FUNCTIONS - -// ============================================================ -// 2. Include Wuffs Implementation (ONLY HERE) -// ============================================================ -#define WUFFS_IMPLEMENTATION -#include "../third_party/wuffs/release/c/wuffs-v0.4.c" - -// ============================================================ -// 3. Wrapper Functions for QuickView -// ============================================================ -#include "WuffsLoader.h" -#include -#include - -namespace WuffsLoader { - -// ------------------------------------------------------------ -// PNG Decoder -// ------------------------------------------------------------ -bool DecodePNG(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_png__decoder dec; - wuffs_base__status status = wuffs_png__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_png__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_png__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_png__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_png__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -// ------------------------------------------------------------ -// GIF Decoder -// ------------------------------------------------------------ -bool DecodeGIF(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_gif__decoder dec; - wuffs_base__status status = wuffs_gif__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_gif__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_gif__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_gif__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_gif__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -// ------------------------------------------------------------ -// BMP Decoder -// ------------------------------------------------------------ -bool DecodeBMP(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_bmp__decoder dec; - wuffs_base__status status = wuffs_bmp__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_bmp__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_bmp__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_bmp__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_bmp__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -// ------------------------------------------------------------ -// TGA Decoder (Note the usage of wuffs_targa namespace) -// ------------------------------------------------------------ -bool DecodeTGA(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_targa__decoder dec; - wuffs_base__status status = wuffs_targa__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_targa__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_targa__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_targa__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_targa__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -// ------------------------------------------------------------ -// WBMP Decoder -// ------------------------------------------------------------ -bool DecodeWBMP(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_wbmp__decoder dec; - wuffs_base__status status = wuffs_wbmp__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_wbmp__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_wbmp__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_wbmp__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_wbmp__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -// ------------------------------------------------------------ -// NetPBM Decoder (PAM, PBM, PGM, PPM) -// ------------------------------------------------------------ -bool DecodeNetpbm(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_netpbm__decoder dec; - wuffs_base__status status = wuffs_netpbm__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_netpbm__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_netpbm__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_netpbm__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_netpbm__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -// ------------------------------------------------------------ -// QOI Decoder (Quite OK Image) -// ------------------------------------------------------------ -bool DecodeQOI(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels) { - wuffs_qoi__decoder dec; - wuffs_base__status status = wuffs_qoi__decoder__initialize( - &dec, sizeof(dec), WUFFS_VERSION, - WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED); - if (!wuffs_base__status__is_ok(&status)) return false; - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader(const_cast(data), size, true); - - wuffs_base__image_config ic = {0}; - status = wuffs_qoi__decoder__decode_image_config(&dec, &ic, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint32_t width = wuffs_base__pixel_config__width(&ic.pixcfg); - uint32_t height = wuffs_base__pixel_config__height(&ic.pixcfg); - if (width == 0 || height == 0) return false; - - wuffs_base__pixel_config__set(&ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); - - size_t stride = width * 4; - size_t pixelSize = stride * height; - outPixels.resize(pixelSize); - - wuffs_base__pixel_buffer pb; - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(outPixels.data(), pixelSize)); - if (!wuffs_base__status__is_ok(&status)) return false; - - uint64_t workbuf_len = wuffs_qoi__decoder__workbuf_len(&dec).max_incl; - std::vector workbuf(workbuf_len); - - wuffs_base__frame_config fc = {0}; - status = wuffs_qoi__decoder__decode_frame_config(&dec, &fc, &src); - if (!wuffs_base__status__is_ok(&status)) return false; - - status = wuffs_qoi__decoder__decode_frame(&dec, &pb, &src, WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(workbuf.data(), workbuf.size()), nullptr); - if (!wuffs_base__status__is_ok(&status)) return false; - - *outWidth = width; - *outHeight = height; - return true; -} - -} // namespace WuffsLoader diff --git a/QuickView/WuffsLoader.h b/QuickView/WuffsLoader.h deleted file mode 100644 index cac44cbc..00000000 --- a/QuickView/WuffsLoader.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once -// ============================================================ -// WuffsLoader.h - Wuffs Wrapper API for QuickView -// ============================================================ -// This header exposes clean C++ functions for PNG/GIF decoding. -// The actual Wuffs implementation is hidden in WuffsImpl.cpp. - -#include -#include - -namespace WuffsLoader { - -/// -/// Decode PNG image to BGRA pixels -/// -/// PNG file data -/// Data size in bytes -/// Output: image width -/// Output: image height -/// Output: BGRA pixel data (stride = width * 4) -/// true on success -bool DecodePNG(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -/// -/// Decode GIF image (first frame) to BGRA pixels -/// -/// GIF file data -/// Data size in bytes -/// Output: image width -/// Output: image height -/// Output: BGRA pixel data (stride = width * 4) -/// true on success -bool DecodeGIF(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -/// -/// Decode BMP image to BGRA pixels -/// -bool DecodeBMP(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -/// -/// Decode TGA (Targa) image to BGRA pixels -/// -bool DecodeTGA(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -/// -/// Decode WBMP image to BGRA pixels -/// -bool DecodeWBMP(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -/// -/// Decode NetPBM (PAM, PBM, PGM, PPM) image to BGRA pixels -/// -bool DecodeNetpbm(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -/// -/// Decode QOI (Quite OK Image) to BGRA pixels -/// -bool DecodeQOI(const uint8_t* data, size_t size, - uint32_t* outWidth, uint32_t* outHeight, - std::vector& outPixels); - -} // namespace WuffsLoader diff --git a/QuickView/build_error.txt b/QuickView/build_error.txt deleted file mode 100644 index a10a2e8e..00000000 --- a/QuickView/build_error.txt +++ /dev/null @@ -1,13 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework - -D:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Microsoft\VC\v180\Microsoft.CppBuild.targets(548,5): warning MSB8028: The intermediate directory (E:\Personal\Coding\QuickView\QuickView\obj\x64\Release\) contains files shared from another project (QuickView2026.vcxproj). This can lead to incorrect clean and rebuild behavior. [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] - main.cpp -E:\Personal\Coding\QuickView\QuickView\FileNavigator.h(62,64): warning C4267: “=â€�: 从“size_tâ€�转æ�¢åˆ°â€œintâ€�,å�¯èƒ½ä¸¢å¤±æ•°æ�® [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] - (compiling source file '/main.cpp') - -E:\Personal\Coding\QuickView\QuickView\main.cpp(293,58): warning C4100: 'clientRect': 未引用的å�‚æ•° [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] - (compiling source file '/main.cpp') - -E:\Personal\Coding\QuickView\QuickView\main.cpp(519,38): error C2065: “FW_SEMI_BOLDâ€�: 未声明的标识符 [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] - (compiling source file '/main.cpp') - diff --git a/QuickView/build_error_log.txt b/QuickView/build_error_log.txt deleted file mode 100644 index 4e5cbb1a..00000000 --- a/QuickView/build_error_log.txt +++ /dev/null @@ -1,44 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework -Build started 2025/12/21 12:30:48. - - 1>Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" on node 1 (Build target(s)). - 1>PrepareForBuild: - Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. - InitializeBuildStatus: - Touching "E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.tlog\unsuccessfulbuild". - ComputeStdModulesCompileInputs: - Reading library manifest file 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\modules.json' which provides data for library 'microsoft/STL'. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.ixx' from library 'microsoft/STL' to be built with this project. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.compat.ixx' from library 'microsoft/STL' to be built with this project. - VcpkgTripletSelection: - Using triplet "x64-windows-static-opt" from "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\" - Using normalized configuration "Release" - ClCompile: - All outputs are up-to-date. - All outputs are up-to-date. - All outputs are up-to-date. - Link: - D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\bin\HostX86\x64\link.exe /ERRORREPORT:QUEUE /OUT:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe" /NOLOGO /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib" /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\manual-link" dwrite.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\*.lib" /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.pdb" /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /LTCG:incremental /LTCGOUT:"E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.iobj" /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib" /MACHINE:X64 E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\pch.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\RenderEngine.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\ImageLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\LosslessTransform.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\StbLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\TinyExrLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\WuffsImpl.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\main.obj - ÕýÔÚ´´½¨¿â E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib ºÍ¶ÔÏó E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exp - 1>StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_buffer [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>Done Building Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target(s)) -- FAILED. - -Build FAILED. - - "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target) (1) -> - (Link target) -> - StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_buffer [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - - 0 Warning(s) - 2 Error(s) - -Time Elapsed 00:00:01.26 diff --git a/QuickView/build_fail_2.txt b/QuickView/build_fail_2.txt deleted file mode 100644 index 0f65982f..00000000 --- a/QuickView/build_fail_2.txt +++ /dev/null @@ -1,10 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework - -D:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Microsoft\VC\v180\Microsoft.CppBuild.targets(548,5): warning MSB8028: The intermediate directory (E:\Personal\Coding\QuickView\QuickView\obj\x64\Release\) contains files shared from another project (QuickView2026.vcxproj). This can lead to incorrect clean and rebuild behavior. [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] - 正在创建库 E:\Personal\Coding\QuickView\QuickView\bin\x64\Release\QuickView.lib 和对象 E:\Personal\Coding\QuickView\QuickView\bin\x64\Release\QuickView.exp - 正在生æˆ�代ç � - 9 of 24783 functions (<0.1%) were compiled, the rest were copied from previous compilation. - 0 functions were new in current compilation - 434 functions had inline decision re-evaluated but remain unchanged - 已完æˆ�代ç �的生æˆ� - QuickView.vcxproj -> E:\Personal\Coding\QuickView\QuickView\bin\x64\Release\QuickView.exe diff --git a/QuickView/build_fail_round.txt b/QuickView/build_fail_round.txt deleted file mode 100644 index 84df1a06..00000000 --- a/QuickView/build_fail_round.txt +++ /dev/null @@ -1,4 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework - -D:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Microsoft\VC\v180\Microsoft.CppBuild.targets(548,5): warning MSB8028: The intermediate directory (E:\Personal\Coding\QuickView\QuickView\obj\x64\Release\) contains files shared from another project (QuickView2026.vcxproj). This can lead to incorrect clean and rebuild behavior. [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] -LINK : fatal error LNK1104: 无法打开文件“E:\Personal\Coding\QuickView\QuickView\bin\x64\Release\QuickView.exeâ€� [E:\Personal\Coding\QuickView\QuickView\QuickView.vcxproj] diff --git a/QuickView/build_output.txt b/QuickView/build_output.txt deleted file mode 100644 index cce7fd65..00000000 --- a/QuickView/build_output.txt +++ /dev/null @@ -1,29 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework - - pch.cpp - RenderEngine.cpp - ImageLoader.cpp - LosslessTransform.cpp - main.cpp -e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(145,55): warning C4100: 'clientRect': δÒýÓõIJÎÊý [e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\QuickView2026.vcxproj] - (compiling source file '/main.cpp') - -e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(232,58): warning C4100: 'clientRect': δÒýÓõIJÎÊý [e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\QuickView2026.vcxproj] - (compiling source file '/main.cpp') - -e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(630,9): error C2065: ¡°argc¡±: δÉùÃ÷µÄ±êʶ·û [e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\QuickView2026.vcxproj] - (compiling source file '/main.cpp') - -e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(631,30): error C2065: ¡°argv¡±: δÉùÃ÷µÄ±êʶ·û [e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\QuickView2026.vcxproj] - (compiling source file '/main.cpp') - -e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(631,9): error C2660: ¡°LoadImageAsync¡±: º¯Êý²»½ÓÊÜ 1 ¸ö²ÎÊý [e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\QuickView2026.vcxproj] - (compiling source file '/main.cpp') - e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(98,15): - ²Î¼û¡°LoadImageAsync¡±µÄÉùÃ÷ - e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(631,9): - ³¢ÊÔÆ¥Åä²ÎÊýÁÐ±í¡°(HWND)¡±Ê± - -e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\main.cpp(633,15): error C2065: ¡°argv¡±: δÉùÃ÷µÄ±êʶ·û [e:\Personal\Coding\QuickView\QuickView_VS2026_X64\src\QuickView2026\QuickView2026.vcxproj] - (compiling source file '/main.cpp') - diff --git a/QuickView/framework.h b/QuickView/framework.h deleted file mode 100644 index 8651b8ba..00000000 --- a/QuickView/framework.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "targetver.h" -#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers -#define NOMINMAX -// Windows Header Files -#include -// C RunTime Header Files -#include -#include -#include -#include -#include "CoroutineTypes.h" diff --git a/QuickView/main.cpp b/QuickView/main.cpp deleted file mode 100644 index cb8acc10..00000000 --- a/QuickView/main.cpp +++ /dev/null @@ -1,3357 +0,0 @@ -#include "pch.h" -#include "framework.h" -#include "QuickView.h" -#include "RenderEngine.h" -#include "ImageLoader.h" -#include "LosslessTransform.h" -#include "EditState.h" -#include "AppStrings.h" -#include "ContextMenu.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "OSDState.h" - -#pragma comment(lib, "d2d1.lib") -#pragma comment(lib, "d3d11.lib") -#pragma comment(lib, "dxgi.lib") -#pragma comment(lib, "windowscodecs.lib") -#pragma comment(lib, "dwrite.lib") -#include - -using namespace Microsoft::WRL; - -#include -#include -#include -#include // For IDesktopWallpaper -#include -#include -#include -#include -#include -#include -#pragma comment(lib, "dwmapi.lib") -#pragma comment(lib, "Shcore.lib") -#pragma comment(lib, "winspool.lib") -#pragma comment(lib, "comctl32.lib") - -#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") - -// --- Dialog & OSD Definitions --- - -enum class DialogResult { None, Yes, No, Cancel, Custom1 }; - -struct DialogButton { - DialogResult Result; - std::wstring Text; - bool IsDefault; - DialogButton(DialogResult r, const wchar_t* t, bool d = false) : Result(r), Text(t), IsDefault(d) {} - DialogButton(const DialogButton&) = default; - DialogButton(DialogButton&&) = default; - DialogButton& operator=(const DialogButton&) = default; - DialogButton& operator=(DialogButton&&) = default; -}; - -struct DialogLayout { - D2D1_RECT_F Box; - D2D1_RECT_F Checkbox; - std::vector Buttons; -}; - -struct DialogState { - bool IsVisible = false; - std::wstring Title; - std::wstring Message; - std::wstring QualityText; // New field for quality info - D2D1_COLOR_F AccentColor = D2D1::ColorF(D2D1::ColorF::DodgerBlue); - std::vector Buttons; - int SelectedButtonIndex = 0; - bool HasCheckbox = false; - std::wstring CheckboxText; - bool IsChecked = false; - DialogResult FinalResult = DialogResult::None; -}; - -// struct OSDState moved to OSDState.h - -struct ViewState { - float Zoom = 1.0f; - float PanX = 0.0f; - float PanY = 0.0f; - bool IsDragging = false; - bool IsInteracting = false; // True during drag/zoom/resize for dynamic interpolation - POINT LastMousePos = { 0, 0 }; - POINT DragStartPos = { 0, 0 }; // For click vs drag detection - DWORD DragStartTime = 0; // For click vs drag detection - - void Reset() { Zoom = 1.0f; PanX = 0.0f; PanY = 0.0f; IsDragging = false; IsInteracting = false; } -}; - -#include "FileNavigator.h" -#include "GalleryOverlay.h" -#include "Toolbar.h" - -// --- Globals --- - -static const wchar_t* g_szClassName = L"QuickViewClass"; -static const wchar_t* g_szWindowTitle = L"QuickView 2026"; -static std::unique_ptr g_renderEngine; -static std::unique_ptr g_imageLoader; -static ComPtr g_currentBitmap; -static std::wstring g_imagePath; -OSDState g_osd; // Removed static, explicitly Global -DWORD g_toolbarHideTime = 0; // For auto-hide delay -static DialogState g_dialog; -static EditState g_editState; -static AppConfig g_config; -static ViewState g_viewState; -static FileNavigator g_navigator; // New Navigator -static ThumbnailManager g_thumbMgr; -static GalleryOverlay g_gallery; -static Toolbar g_toolbar; -static CImageLoader::ImageMetadata g_currentMetadata; -static ComPtr g_prefetchedBitmap; -static std::wstring g_prefetchedPath; -static ComPtr g_pPanelTextFormat; -static D2D1_RECT_F g_gpsLinkRect = {}; -static D2D1_RECT_F g_gpsCoordRect = {}; // GPS Coordinates click area -static D2D1_RECT_F g_filenameRect = {}; // Filename click area -static D2D1_RECT_F g_panelToggleRect = {}; // Expand/Collapse Button Rect -static D2D1_RECT_F g_panelCloseRect = {}; // Close Button Rect - -// Helper: Copy text to clipboard -static bool CopyToClipboard(HWND hwnd, const std::wstring& text) { - if (!OpenClipboard(hwnd)) return false; - EmptyClipboard(); - size_t size = (text.length() + 1) * sizeof(wchar_t); - HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size); - if (hMem) { - memcpy(GlobalLock(hMem), text.c_str(), size); - GlobalUnlock(hMem); - SetClipboardData(CF_UNICODETEXT, hMem); - } - CloseClipboard(); - return hMem != nullptr; -} - -// Helper: Format bytes with thousand separators (e.g., 1,138,997 B) -static std::wstring FormatBytesWithCommas(UINT64 bytes) { - std::wstring num = std::to_wstring(bytes); - int insertPos = (int)num.length() - 3; - while (insertPos > 0) { - num.insert(insertPos, L","); - insertPos -= 3; - } - return num + L" B"; -} - -// ============================================================================ -// UI Grid System - Data-Driven Info Panel -// ============================================================================ - -enum class TruncateMode { - None, // No truncation - EndEllipsis, // End truncation: "Canon EF 24-70mm..." - MiddleEllipsis, // Middle truncation: "Project_A...Final.psd" -}; - -struct InfoRow { - std::wstring icon; // Emoji icon (e.g., "📄") - std::wstring label; // Label (e.g., "文件") - std::wstring valueMain; // Main value (e.g., "f/1.6") - std::wstring valueSub; // Secondary value in gray (e.g., "(1,138,997 B)") - std::wstring fullText; // Full text for tooltip - - TruncateMode mode = TruncateMode::EndEllipsis; - bool isClickable = false; - - // Runtime (calculated during layout) - D2D1_RECT_F hitRect = {}; - std::wstring displayText; // Truncated display text - bool isTruncated = false; -}; - -// Grid state -static std::vector g_infoGrid; -static int g_hoverRowIndex = -1; // -1 = no hover -static POINT g_lastMousePos = {}; - -// Grid layout constants -static const float GRID_ICON_WIDTH = 20.0f; -static const float GRID_LABEL_WIDTH = 55.0f; -static const float GRID_ROW_HEIGHT = 18.0f; -static const float GRID_PADDING = 8.0f; - -static float MeasureTextWidth(const std::wstring& text, IDWriteTextFormat* format); - -// Smart truncation: measure text width and truncate accordingly -static std::wstring MakeMiddleEllipsis(IDWriteTextFormat* format, float maxWidth, const std::wstring& text) { - if (text.empty() || !format || !g_renderEngine) return text; - - float fullWidth = MeasureTextWidth(text, format); - if (fullWidth <= maxWidth) return text; - - // Binary search for optimal truncation - float ellipsisWidth = MeasureTextWidth(L"...", format); - float availWidth = maxWidth - ellipsisWidth; - if (availWidth <= 0) return L"..."; - - // Approximate: keep 1/3 at start, 2/3 at end - size_t total = text.length(); - size_t keepStart = total / 3; - size_t keepEnd = total / 2; - - // Refine by measuring - for (int i = 0; i < 5; i++) { // Max 5 iterations - std::wstring candidate = text.substr(0, keepStart) + L"..." + text.substr(total - keepEnd); - float width = MeasureTextWidth(candidate, format); - if (width <= maxWidth) { - return candidate; - } - keepStart = keepStart * 9 / 10; - keepEnd = keepEnd * 9 / 10; - if (keepStart < 3 || keepEnd < 3) break; - } - - // Fallback: very short - if (total > 10) { - return text.substr(0, 4) + L"..." + text.substr(total - 4); - } - return text.substr(0, 3) + L"..."; -} - -static std::wstring MakeEndEllipsis(IDWriteTextFormat* format, float maxWidth, const std::wstring& text) { - if (text.empty() || !format || !g_renderEngine) return text; - - float fullWidth = MeasureTextWidth(text, format); - if (fullWidth <= maxWidth) return text; - - float ellipsisWidth = MeasureTextWidth(L"...", format); - float availWidth = maxWidth - ellipsisWidth; - if (availWidth <= 0) return L"..."; - - // Binary search - size_t lo = 0, hi = text.length(); - while (lo < hi) { - size_t mid = (lo + hi + 1) / 2; - float w = MeasureTextWidth(text.substr(0, mid), format); - if (w <= availWidth) lo = mid; - else hi = mid - 1; - } - - if (lo > 0) return text.substr(0, lo) + L"..."; - return L"..."; -} - - - -static float MeasureTextWidth(const std::wstring& text, IDWriteTextFormat* format) { - if (text.empty() || !format) return 0.0f; - ComPtr layout; - g_renderEngine->m_dwriteFactory->CreateTextLayout( - text.c_str(), (UINT32)text.length(), format, - 5000.0f, 5000.0f, // Large max width/height - &layout - ); - if (!layout) return 0.0f; - DWRITE_TEXT_METRICS metrics; - layout->GetMetrics(&metrics); - return metrics.width; -} - - -// --- Forward Declarations --- -LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); -void OnPaint(HWND hwnd); -void OnResize(HWND hwnd, UINT width, UINT height); -FireAndForget LoadImageAsync(HWND hwnd, std::wstring path); -FireAndForget UpdateHistogramAsync(HWND hwnd, std::wstring path); -void ReloadCurrentImage(HWND hwnd); -void Navigate(HWND hwnd, int direction); -void ReleaseImageResources(); -void DiscardChanges(); -std::wstring ShowRenameDialog(HWND hParent, const std::wstring& oldName); - -// Helper: Check if panning makes sense (image exceeds window OR window exceeds screen) -bool CanPan(HWND hwnd) { - if (!g_currentBitmap) return false; - - RECT rc; GetClientRect(hwnd, &rc); - float windowW = (float)(rc.right - rc.left); - float windowH = (float)(rc.bottom - rc.top); - - D2D1_SIZE_F imgSize = g_currentBitmap->GetSize(); - float fitScale = std::min(windowW / imgSize.width, windowH / imgSize.height); - float scaledW = imgSize.width * fitScale * g_viewState.Zoom; - float scaledH = imgSize.height * fitScale * g_viewState.Zoom; - - // Condition 0: Always allow pan if zoomed in - if (g_viewState.Zoom > 1.01f) return true; - - // Condition 1: Image exceeds window bounds - if (scaledW > windowW + 1.0f || scaledH > windowH + 1.0f) { - return true; - } - - // Condition 2: Window exceeds screen bounds (locked window mode or zoomed beyond screen) - // Get screen work area (excludes taskbar) - HMONITOR hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; - if (GetMonitorInfo(hMon, &mi)) { - RECT windowRect; GetWindowRect(hwnd, &windowRect); - int winW = windowRect.right - windowRect.left; - int winH = windowRect.bottom - windowRect.top; - int screenW = mi.rcWork.right - mi.rcWork.left; - int screenH = mi.rcWork.bottom - mi.rcWork.top; - - // If window is larger than screen work area in either dimension - if (winW > screenW || winH > screenH) { - return true; - } - } - - return false; -} - -// --- Helper Functions --- - -bool FileExists(LPCWSTR path) { - DWORD dwAttrib = GetFileAttributesW(path); - return (dwAttrib != INVALID_FILE_ATTRIBUTES && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY)); -} - -bool IsRawFile(const std::wstring& path) { - size_t dot = path.find_last_of(L'.'); - if (dot == std::wstring::npos) return false; - std::wstring ext = path.substr(dot); - std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); - - static const wchar_t* rawExts[] = { - L".arw", L".cr2", L".cr3", L".dng", L".nef", L".orf", L".raf", L".rw2", L".srw", L".pef", - L".erf", L".mef", L".mos", L".mrw", L".nrw", L".raw", L".rwl", L".sr2", L".x3f", L".iiq", L".3fr" - }; - for (const auto* e : rawExts) { - if (ext == e) return true; - } - return false; -} - -void ReleaseImageResources() { - g_currentBitmap.Reset(); - Sleep(50); -} - -DialogLayout CalculateDialogLayout(D2D1_SIZE_F size) { - DialogLayout layout; - float dlgW = 420.0f; - - // Calculate required height based on content - // Title line: ~30px, Message lines: estimate based on length, Buttons: 50px, Padding: 40px - float titleHeight = 35.0f; - float messageHeight = 25.0f; - - // Estimate title wrapping (assume ~25 chars per line at this width) - int titleLines = (int)(g_dialog.Title.length() / 22) + 1; - if (titleLines > 3) titleLines = 3; // Max 3 lines - - // Message usually single line - int msgLines = 1; - - float contentHeight = (titleLines * titleHeight) + (msgLines * messageHeight); - float qualityHeight = !g_dialog.QualityText.empty() ? 30.0f : 0.0f; // Add space for quality text - float checkboxHeight = g_dialog.HasCheckbox ? 45.0f : 0.0f; - float buttonsHeight = 55.0f; - float padding = 45.0f; // Increased padding - - float dlgH = padding + contentHeight + qualityHeight + checkboxHeight + buttonsHeight + 30.0f; // More buffer - if (dlgH < 200.0f) dlgH = 200.0f; // Increased minimum height - if (dlgH > 400.0f) dlgH = 400.0f; // Increased maximum height - - float left = (size.width - dlgW) / 2.0f; - float top = (size.height - dlgH) / 2.0f; - layout.Box = D2D1::RectF(left, top, left + dlgW, top + dlgH); - - // Checkbox area (only used if HasCheckbox) - float checkY = top + dlgH - 95; - layout.Checkbox = D2D1::RectF(left + 25, checkY, left + 45, checkY + 20); - - // Buttons area - float btnW = 95.0f; - float btnH = 30.0f; - float btnGap = 12.0f; - float totalBtnWidth = (g_dialog.Buttons.size() * btnW) + ((g_dialog.Buttons.size() - 1) * btnGap); - float startX = left + dlgW - 20 - totalBtnWidth; - if (startX < left + 20) startX = left + 20; // Safety clamp - - float btnY = top + dlgH - 45; - - for (size_t i = 0; i < g_dialog.Buttons.size(); ++i) { - layout.Buttons.push_back(D2D1::RectF(startX + i * (btnW + btnGap), btnY, startX + i * (btnW + btnGap) + btnW, btnY + btnH)); - } - return layout; -} - -// --- Draw Functions --- - -// DrawOSD moved to RenderEngine - -// --- Window Controls --- -enum class WindowHit { None, Pin, Min, Max, Close }; -struct WindowControls { - D2D1_RECT_F PinRect; // Pin (Always on Top) button - leftmost - D2D1_RECT_F MinRect; - D2D1_RECT_F MaxRect; - D2D1_RECT_F CloseRect; - WindowHit HoverState = WindowHit::None; -}; -static WindowControls g_winControls; - -void CalculateWindowControls(D2D1_SIZE_F size) { - float btnW = 46.0f; - float btnH = 32.0f; - g_winControls.CloseRect = D2D1::RectF(size.width - btnW, 0, size.width, btnH); - g_winControls.MaxRect = D2D1::RectF(size.width - btnW * 2, 0, size.width - btnW, btnH); - g_winControls.MinRect = D2D1::RectF(size.width - btnW * 3, 0, size.width - btnW * 2, btnH); - g_winControls.PinRect = D2D1::RectF(size.width - btnW * 4, 0, size.width - btnW * 3, btnH); -} - -static bool g_showControls = false; - -// --- REFACTOR: DrawWindowControls with Segoe Fluent Icons --- -// --- REFACTOR: DrawWindowControls with Segoe Fluent Icons --- -void DrawWindowControls(HWND hwnd, ID2D1DeviceContext* context) { - if (g_config.AutoHideWindowControls && !g_showControls && g_winControls.HoverState == WindowHit::None) return; - - // Backgrounds - if (g_winControls.HoverState == WindowHit::Close) { - ComPtr pRed; - context->CreateSolidColorBrush(D2D1::ColorF(0.9f, 0.1f, 0.1f), &pRed); - context->FillRectangle(g_winControls.CloseRect, pRed.Get()); - } else if (g_winControls.HoverState != WindowHit::None) { - ComPtr pGray; - context->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.1f), &pGray); - if (g_winControls.HoverState == WindowHit::Max) context->FillRectangle(g_winControls.MaxRect, pGray.Get()); - if (g_winControls.HoverState == WindowHit::Min) context->FillRectangle(g_winControls.MinRect, pGray.Get()); - if (g_winControls.HoverState == WindowHit::Pin) context->FillRectangle(g_winControls.PinRect, pGray.Get()); - } - - // Brushes - ComPtr pWhite, pOutline, pPinActive; - context->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f), &pWhite); - context->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.5f), &pOutline); - context->CreateSolidColorBrush(D2D1::ColorF(0.2f, 0.6f, 1.0f), &pPinActive); // Blue when active - - // Create text format for Segoe Fluent Icons - static ComPtr pDW; - static ComPtr iconFormat; - if (!pDW) DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(pDW.GetAddressOf())); - if (pDW && !iconFormat) { - pDW->CreateTextFormat(L"Segoe Fluent Icons", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 12.0f, L"en-us", &iconFormat); - if (iconFormat) { - iconFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); - iconFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); - } - } - - if (iconFormat) { - // Draw Icon Helper - auto DrawIcon = [&](wchar_t icon, D2D1_RECT_F r, ID2D1SolidColorBrush* brush) { - // Outline (Offsets) - float offsets[] = { -1.0f, 1.0f }; - for (float ox : offsets) { - for (float oy : offsets) { - D2D1_RECT_F rOut = D2D1::RectF(r.left + ox, r.top + oy, r.right + ox, r.bottom + oy); - context->DrawText(&icon, 1, iconFormat.Get(), rOut, pOutline.Get()); - } - } - context->DrawText(&icon, 1, iconFormat.Get(), r, brush); - }; - - // Pin - wchar_t pinIcon = g_config.AlwaysOnTop ? L'\uE77A' : L'\uE718'; - DrawIcon(pinIcon, g_winControls.PinRect, g_config.AlwaysOnTop ? pPinActive.Get() : pWhite.Get()); - - // Min - DrawIcon(L'\uE921', g_winControls.MinRect, pWhite.Get()); - - // Max / Restore - // Max / Restore - // HWND is passed in - // Check IsZoomed using passed HWND - wchar_t maxIcon = IsZoomed(hwnd) ? L'\uE923' : L'\uE922'; // Restore : Maximize - DrawIcon(maxIcon, g_winControls.MaxRect, pWhite.Get()); - - // Close - DrawIcon(L'\uE8BB', g_winControls.CloseRect, pWhite.Get()); - } - - - // Tooltip for Pin button - if (g_winControls.HoverState == WindowHit::Pin) { - std::wstring tip = g_config.AlwaysOnTop ? L"Unpin (Ctrl+T)" : L"Always on Top (Ctrl+T)"; - - static ComPtr tipFormat; - if (!tipFormat && pDW) { - pDW->CreateTextFormat(L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 12.0f, L"en-us", &tipFormat); - } - - if (tipFormat && pDW) { - ComPtr layout; - pDW->CreateTextLayout(tip.c_str(), (UINT32)tip.length(), tipFormat.Get(), 200.0f, 50.0f, &layout); - - if (layout) { - DWRITE_TEXT_METRICS metrics; - layout->GetMetrics(&metrics); - - float tipW = metrics.width + 12.0f; - float tipH = metrics.height + 8.0f; - - // Position below the button - D2D1_RECT_F btnRect = g_winControls.PinRect; - float tipX = btnRect.left + (btnRect.right - btnRect.left - tipW) / 2.0f; - float tipY = btnRect.bottom + 5.0f; - - // Tooltip Background - D2D1_RECT_F tipRect = D2D1::RectF(tipX, tipY, tipX + tipW, tipY + tipH); - ComPtr bgBrush, textBrush, borderBrush; - context->CreateSolidColorBrush(D2D1::ColorF(0.15f, 0.15f, 0.15f), &bgBrush); - context->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f), &textBrush); - context->CreateSolidColorBrush(D2D1::ColorF(0.4f, 0.4f, 0.4f), &borderBrush); - - context->FillRoundedRectangle(D2D1::RoundedRect(tipRect, 4.0f, 4.0f), bgBrush.Get()); - context->DrawRoundedRectangle(D2D1::RoundedRect(tipRect, 4.0f, 4.0f), borderBrush.Get(), 1.0f); - - context->DrawTextLayout(D2D1::Point2F(tipX + 6.0f, tipY + 4.0f), layout.Get(), textBrush.Get()); - } - } - } -} - -void DrawDialog(ID2D1DeviceContext* context, const RECT& clientRect) { - if (!g_dialog.IsVisible || !context) return; - - D2D1_SIZE_F size = context->GetSize(); - DialogLayout layout = CalculateDialogLayout(size); - - // Overlay (background dimming) - ComPtr pOverlayBrush; - context->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.6f), &pOverlayBrush); // Slightly clearer overlay - context->FillRectangle(D2D1::RectF(0, 0, size.width, size.height), pOverlayBrush.Get()); - - // Box Background (with configurable alpha) - ComPtr pBgBrush; - context->CreateSolidColorBrush(D2D1::ColorF(0.18f, 0.18f, 0.18f, g_config.DialogAlpha), &pBgBrush); - context->FillRoundedRectangle(D2D1::RoundedRect(layout.Box, 10.0f, 10.0f), pBgBrush.Get()); - - // Border - ComPtr pBorderBrush; - context->CreateSolidColorBrush(g_dialog.AccentColor, &pBorderBrush); - context->DrawRoundedRectangle(D2D1::RoundedRect(layout.Box, 10.0f, 10.0f), pBorderBrush.Get(), 2.0f); - - // Fonts - static ComPtr pDW; - static ComPtr fmtTitle; - static ComPtr fmtBody; - static ComPtr fmtBtn; - static ComPtr fmtBtnCenter; // New centered format - - if (!pDW) DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(pDW.GetAddressOf())); - if (pDW) { - if (!fmtTitle) { - pDW->CreateTextFormat(L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 22.0f, L"en-us", &fmtTitle); - if (fmtTitle) fmtTitle->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); - } - if (!fmtBody) pDW->CreateTextFormat(L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 18.0f, L"en-us", &fmtBody); - if (!fmtBtn) pDW->CreateTextFormat(L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_SEMI_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 16.0f, L"en-us", &fmtBtn); - if (!fmtBtnCenter) { - pDW->CreateTextFormat(L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_SEMI_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 16.0f, L"en-us", &fmtBtnCenter); - if (fmtBtnCenter) fmtBtnCenter->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); - if (fmtBtnCenter) fmtBtnCenter->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); - } - } - - ComPtr pWhite; - context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pWhite); - - // Title (truncate to show end of filename with extension, single line) - std::wstring displayTitle = g_dialog.Title; - const size_t maxLen = 30; - if (displayTitle.length() > maxLen) { - displayTitle = L"..." + displayTitle.substr(displayTitle.length() - (maxLen - 3)); - } - float titleTop = layout.Box.top + 18; - float titleBottom = layout.Box.top + 48; - context->DrawText(displayTitle.c_str(), (UINT32)displayTitle.length(), fmtTitle.Get(), - D2D1::RectF(layout.Box.left + 25, titleTop, layout.Box.right - 25, titleBottom), pWhite.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE); - - // Message (below title with proper spacing) - float msgTop = titleBottom + 8; - // Message ends 25px above QualityText - float msgBottom = layout.Checkbox.top - 55.0f; - context->DrawText(g_dialog.Message.c_str(), (UINT32)g_dialog.Message.length(), fmtBody.Get(), - D2D1::RectF(layout.Box.left + 25, msgTop, layout.Box.right - 25, msgBottom), pWhite.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE); - - // Quality Info (Colored) - positioned with more space above checkbox - if (!g_dialog.QualityText.empty()) { - float qualityY = layout.Checkbox.top - 45.0f; // 45px above checkbox - context->DrawText(g_dialog.QualityText.c_str(), (UINT32)g_dialog.QualityText.length(), fmtBody.Get(), - D2D1::RectF(layout.Box.left + 30, qualityY, layout.Box.right - 30, qualityY + 25), pBorderBrush.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE); - } - - // Checkbox - if (g_dialog.HasCheckbox) { - context->DrawRectangle(layout.Checkbox, pWhite.Get(), 1.0f); - if (g_dialog.IsChecked) { - context->FillRectangle(D2D1::RectF(layout.Checkbox.left+4, layout.Checkbox.top+4, layout.Checkbox.right-4, layout.Checkbox.bottom-4), pBorderBrush.Get()); - } - context->DrawText(g_dialog.CheckboxText.c_str(), (UINT32)g_dialog.CheckboxText.length(), fmtBtn.Get(), - D2D1::RectF(layout.Checkbox.right + 10, layout.Checkbox.top, layout.Box.right - 30, layout.Checkbox.bottom + 5), pWhite.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE); - } - - // Buttons - for (size_t i = 0; i < g_dialog.Buttons.size(); ++i) { - if (i >= layout.Buttons.size()) break; - D2D1_RECT_F btnRect = layout.Buttons[i]; - - if (i == g_dialog.SelectedButtonIndex) { - context->FillRoundedRectangle(D2D1::RoundedRect(btnRect, 4.0f, 4.0f), pBorderBrush.Get()); - } else { - ComPtr pGray; - context->CreateSolidColorBrush(D2D1::ColorF(0.25f, 0.25f, 0.25f), &pGray); - context->FillRoundedRectangle(D2D1::RoundedRect(btnRect, 4.0f, 4.0f), pGray.Get()); - } - - std::wstring& text = g_dialog.Buttons[i].Text; - // Adjust rect slightly for better visual centering (baseline offset) - D2D1_RECT_F textRect = D2D1::RectF(btnRect.left, btnRect.top - 2, btnRect.right, btnRect.bottom - 2); - context->DrawText(text.c_str(), (UINT32)text.length(), fmtBtnCenter.Get(), textRect, pWhite.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE, DWRITE_MEASURING_MODE_NATURAL); - } -} - -// --- Modal Dialog Loop --- - -DialogResult ShowQuickViewDialog(HWND hwnd, const std::wstring& title, const std::wstring& messageContent, - D2D1_COLOR_F accentColor, const std::vector& buttons, - bool hasChecbox = false, const std::wstring& checkboxText = L"", const std::wstring& qualityText = L"") -{ - g_dialog.IsVisible = true; - g_dialog.Title = title; - g_dialog.Message = messageContent; - g_dialog.QualityText = qualityText; - g_dialog.AccentColor = accentColor; - g_dialog.Buttons = buttons; - g_dialog.SelectedButtonIndex = 0; - g_dialog.HasCheckbox = hasChecbox; - g_dialog.CheckboxText = checkboxText; - g_dialog.IsChecked = false; - g_dialog.FinalResult = DialogResult::None; - - InvalidateRect(hwnd, nullptr, FALSE); - UpdateWindow(hwnd); - - MSG msgStruct; - while (g_dialog.IsVisible && GetMessage(&msgStruct, NULL, 0, 0)) { - if (msgStruct.message == WM_KEYDOWN) { - if (msgStruct.wParam == VK_LEFT) { - if (g_dialog.SelectedButtonIndex > 0) g_dialog.SelectedButtonIndex--; - InvalidateRect(hwnd, nullptr, FALSE); - } else if (msgStruct.wParam == VK_RIGHT) { - if (g_dialog.SelectedButtonIndex < g_dialog.Buttons.size() - 1) g_dialog.SelectedButtonIndex++; - InvalidateRect(hwnd, nullptr, FALSE); - } else if (msgStruct.wParam == VK_TAB || msgStruct.wParam == VK_SPACE) { - if (g_dialog.HasCheckbox) { - g_dialog.IsChecked = !g_dialog.IsChecked; - InvalidateRect(hwnd, nullptr, FALSE); - } - } else if (msgStruct.wParam == VK_RETURN) { - g_dialog.FinalResult = g_dialog.Buttons[g_dialog.SelectedButtonIndex].Result; - g_dialog.IsVisible = false; - } else if (msgStruct.wParam == VK_ESCAPE) { - g_dialog.FinalResult = DialogResult::None; - g_dialog.IsVisible = false; - } - } else if (msgStruct.message == WM_LBUTTONDOWN) { - RECT clientRect; GetClientRect(hwnd, &clientRect); - D2D1_SIZE_F size = D2D1::SizeF((float)(clientRect.right - clientRect.left), (float)(clientRect.bottom - clientRect.top)); - DialogLayout layout = CalculateDialogLayout(size); - - float mouseX = (float)((short)LOWORD(msgStruct.lParam)); - float mouseY = (float)((short)HIWORD(msgStruct.lParam)); - - bool handled = false; - // Check checkbox - if (g_dialog.HasCheckbox) { - // Expand hit area slightly - D2D1_RECT_F checkHit = D2D1::RectF(layout.Checkbox.left - 5, layout.Checkbox.top - 5, layout.Box.right - 20, layout.Checkbox.bottom + 5); - if (mouseX >= checkHit.left && mouseX <= checkHit.right && mouseY >= checkHit.top && mouseY <= checkHit.bottom) { - g_dialog.IsChecked = !g_dialog.IsChecked; - InvalidateRect(hwnd, nullptr, FALSE); - handled = true; - } - } - if (!handled) { - for (size_t i = 0; i < layout.Buttons.size(); ++i) { - if (mouseX >= layout.Buttons[i].left && mouseX <= layout.Buttons[i].right && - mouseY >= layout.Buttons[i].top && mouseY <= layout.Buttons[i].bottom) { - g_dialog.SelectedButtonIndex = (int)i; - g_dialog.FinalResult = g_dialog.Buttons[i].Result; - g_dialog.IsVisible = false; - break; - } - } - } - } else if (msgStruct.message == WM_MOUSEMOVE) { - // Optional: hover effect (update SelectedButtonIndex based on mouse pos) - // For now, let's keep it simple. - } - - if (msgStruct.message == WM_PAINT) { - TranslateMessage(&msgStruct); DispatchMessage(&msgStruct); - } else { - TranslateMessage(&msgStruct); DispatchMessage(&msgStruct); - } - } - - InvalidateRect(hwnd, nullptr, FALSE); - return g_dialog.FinalResult; -} - -// --- Rename Dialog (Pseudo-OSD) --- -static std::wstring g_renameResult; -static HWND g_hRenameEdit; -static WNDPROC g_oldEditProc; - -// Custom Button State -static int g_hoverBtn = -1; // 0=OK, 1=Cancel, -1=None -static bool g_isMouseDown = false; -static const RECT g_rcOk = { 115, 60, 195, 86 }; -static const RECT g_rcCancel = { 205, 60, 285, 86 }; - -// Subclass procedure for the Edit control to handle Enter/Esc -LRESULT CALLBACK EditSubclassProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - switch (msg) { - case WM_KEYDOWN: - if (wParam == VK_RETURN) { - SendMessage(GetParent(hwnd), WM_COMMAND, IDOK, 0); - return 0; - } - if (wParam == VK_ESCAPE) { - SendMessage(GetParent(hwnd), WM_COMMAND, IDCANCEL, 0); - return 0; - } - break; - } - return CallWindowProc(g_oldEditProc, hwnd, msg, wParam, lParam); -} - -static HBRUSH g_hDarkBrush = nullptr; - -LRESULT CALLBACK RenameWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - switch (msg) { - case WM_CTLCOLOREDIT: { - HDC hdc = (HDC)wParam; - SetBkColor(hdc, RGB(30, 30, 30)); // Dark Gray - SetTextColor(hdc, RGB(255, 255, 255)); // White - if (!g_hDarkBrush) g_hDarkBrush = CreateSolidBrush(RGB(30,30,30)); - return (LRESULT)g_hDarkBrush; - } - case WM_CREATE: { - NONCLIENTMETRICSW ncm = { sizeof(NONCLIENTMETRICSW) }; - SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0); - ncm.lfMessageFont.lfHeight = -24; // 24px height - ncm.lfMessageFont.lfWeight = 600; // Semi-bold - HFONT hFont = CreateFontIndirectW(&ncm.lfMessageFont); - - // Edit Control moved down slightly, taller to avoid clipping - g_hRenameEdit = CreateWindowW(L"EDIT", L"", WS_VISIBLE | WS_CHILD | ES_CENTER | ES_AUTOHSCROLL, - 10, 15, 380, 30, hwnd, (HMENU)100, GetModuleHandle(nullptr), nullptr); - - SendMessage(g_hRenameEdit, WM_SETFONT, (WPARAM)hFont, TRUE); - - // Subclass to hijack Enter/Esc - g_oldEditProc = (WNDPROC)SetWindowLongPtr(g_hRenameEdit, GWLP_WNDPROC, (LONG_PTR)EditSubclassProc); - - g_hoverBtn = -1; - g_isMouseDown = false; - return 0; - } - case WM_PAINT: { - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - - // Background - HBRUSH hBg = CreateSolidBrush(RGB(30, 30, 30)); - FillRect(hdc, &ps.rcPaint, hBg); - DeleteObject(hBg); - - // Draw Buttons - auto DrawBtn = [&](const RECT& rc, const wchar_t* text, int id) { - bool isHover = (g_hoverBtn == id); - COLORREF bgCol = isHover ? (g_isMouseDown ? RGB(80, 80, 80) : RGB(60, 60, 60)) : RGB(45, 45, 45); - HBRUSH hBr = CreateSolidBrush(bgCol); - FillRect(hdc, &rc, hBr); - DeleteObject(hBr); - - // Border (optional, maybe just flat) - // FrameRect(hdc, &rc, (HBRUSH)GetStockObject(LTGRAY_BRUSH)); - - SetBkMode(hdc, TRANSPARENT); - SetTextColor(hdc, RGB(220, 220, 220)); - DrawTextW(hdc, text, -1, (LPRECT)&rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); - }; - - // Use system font for buttons - HFONT hOldFont = (HFONT)SelectObject(hdc, GetStockObject(DEFAULT_GUI_FONT)); - // Or better, create a specific font? Standard GUI font is ugly. - // Let's use the one from Edit control or similar? - // Ideally we should cache the button font. For now, DEFAULT_GUI_FONT is safe but ugly. - // Actually, let's re-create the button font on the fly or better cache it. - // To be safe and quick, I'll use simple variable. - - NONCLIENTMETRICSW ncm = { sizeof(NONCLIENTMETRICSW) }; - SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0); - ncm.lfMessageFont.lfHeight = -16; // Button font - HFONT hBtnFont = CreateFontIndirectW(&ncm.lfMessageFont); - SelectObject(hdc, hBtnFont); - - DrawBtn(g_rcOk, L"OK", 0); - DrawBtn(g_rcCancel, L"Cancel", 1); - - SelectObject(hdc, hOldFont); - DeleteObject(hBtnFont); - - EndPaint(hwnd, &ps); - return 0; - } - case WM_MOUSEMOVE: { - POINT pt = { LOWORD(lParam), HIWORD(lParam) }; - int oldHover = g_hoverBtn; - if (PtInRect(&g_rcOk, pt)) g_hoverBtn = 0; - else if (PtInRect(&g_rcCancel, pt)) g_hoverBtn = 1; - else g_hoverBtn = -1; - - if (oldHover != g_hoverBtn) { - InvalidateRect(hwnd, nullptr, FALSE); // Redraw - - if (g_hoverBtn != -1) { // Track mouse leave - TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hwnd, 0 }; - TrackMouseEvent(&tme); - } - } - return 0; - } - case WM_MOUSELEAVE: { - if (g_hoverBtn != -1) { - g_hoverBtn = -1; - InvalidateRect(hwnd, nullptr, FALSE); - } - return 0; - } - case WM_LBUTTONDOWN: { - POINT pt = { LOWORD(lParam), HIWORD(lParam) }; - if (PtInRect(&g_rcOk, pt) || PtInRect(&g_rcCancel, pt)) { - g_isMouseDown = true; - SetCapture(hwnd); - InvalidateRect(hwnd, nullptr, FALSE); - } - return 0; - } - case WM_LBUTTONUP: { - if (g_isMouseDown) { - g_isMouseDown = false; - ReleaseCapture(); - InvalidateRect(hwnd, nullptr, FALSE); - - POINT pt = { LOWORD(lParam), HIWORD(lParam) }; - if (g_hoverBtn == 0 && PtInRect(&g_rcOk, pt)) { - SendMessage(hwnd, WM_COMMAND, IDOK, 0); - } else if (g_hoverBtn == 1 && PtInRect(&g_rcCancel, pt)) { - SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0); - } - } - return 0; - } - case WM_COMMAND: { - if (LOWORD(wParam) == IDOK) { - int len = GetWindowTextLengthW(g_hRenameEdit); - std::vector buf(len + 1); - if (len > 0) GetWindowTextW(g_hRenameEdit, &buf[0], len + 1); - g_renameResult = buf.data() ? buf.data() : L""; - DestroyWindow(hwnd); - return 0; - } - if (LOWORD(wParam) == IDCANCEL) { - g_renameResult.clear(); - DestroyWindow(hwnd); - return 0; - } - break; - } - case WM_ACTIVATE: - // Keep focus on edit if activated - if (LOWORD(wParam) != WA_INACTIVE) { - SetFocus(g_hRenameEdit); - } - break; - case WM_CLOSE: - g_renameResult.clear(); - DestroyWindow(hwnd); - return 0; - case WM_DESTROY: - g_thumbMgr.Shutdown(); - PostQuitMessage(0); - return 0; - } - return DefWindowProc(hwnd, msg, wParam, lParam); -} - -std::wstring ShowRenameDialog(HWND hParent, const std::wstring& oldName) { - g_renameResult.clear(); - - WNDCLASSEXW wc = { sizeof(wc) }; - wc.lpfnWndProc = RenameWndProc; - wc.hInstance = GetModuleHandle(nullptr); - wc.lpszClassName = L"QuickViewRenameOSD"; - wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 30)); // Match Edit BG - wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - RegisterClassExW(&wc); - - RECT rcOwner; GetWindowRect(hParent, &rcOwner); - int w = 400, h = 100; // Taller for better layout - int x = rcOwner.left + (rcOwner.right - rcOwner.left - w) / 2; - int y = rcOwner.top + (rcOwner.bottom - rcOwner.top - h) / 2; - - // WS_POPUP, No Border, TopMost (Removed WS_EX_LAYERED to keep buttons opaque) - HWND hDlg = CreateWindowExW(WS_EX_TOPMOST | WS_EX_TOOLWINDOW, L"QuickViewRenameOSD", L"", - WS_POPUP | WS_VISIBLE, x, y, w, h, hParent, nullptr, wc.hInstance, nullptr); - - // Enable rounded corners (Windows 11) - // Constants: DWMWA_WINDOW_CORNER_PREFERENCE = 33, DWMWCP_ROUND = 2 - enum { DWMWA_WINDOW_CORNER_PREFERENCE_LOCAL = 33 }; - enum { DWMWCP_ROUND_LOCAL = 2 }; - int cornerPref = DWMWCP_ROUND_LOCAL; - DwmSetWindowAttribute(hDlg, DWMWA_WINDOW_CORNER_PREFERENCE_LOCAL, &cornerPref, sizeof(cornerPref)); - - SetWindowTextW(g_hRenameEdit, oldName.c_str()); - SendMessage(g_hRenameEdit, EM_SETSEL, 0, -1); - SetFocus(g_hRenameEdit); - - EnableWindow(hParent, FALSE); - - MSG msg; - while (GetMessage(&msg, nullptr, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - - EnableWindow(hParent, TRUE); - SetForegroundWindow(hParent); - return g_renameResult; -} - -// --- Logic Functions --- - -bool SaveCurrentImage(bool saveAs) { - if (!g_editState.IsDirty || g_editState.TempFilePath.empty()) return true; - ReleaseImageResources(); - - std::wstring targetPath = g_editState.OriginalFilePath; - if (saveAs) { - OPENFILENAMEW ofn = { sizeof(ofn) }; - wchar_t szFile[MAX_PATH] = { 0 }; - wcscpy_s(szFile, g_editState.OriginalFilePath.c_str()); - ofn.lStructSize = sizeof(ofn); - ofn.hwndOwner = GetActiveWindow(); - ofn.lpstrFile = szFile; - ofn.nMaxFile = sizeof(szFile); - ofn.lpstrFilter = L"JPEG Files\0*.jpg;*.jpeg\0PNG Files\0*.png\0All Files\0*.*\0"; - ofn.nFilterIndex = 1; ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT; - if (GetSaveFileNameW(&ofn)) targetPath = szFile; - else { ReloadCurrentImage(GetActiveWindow()); return false; } - } - - bool success = false; - if (targetPath == g_editState.OriginalFilePath && !saveAs) { - if (MoveFileExW(g_editState.TempFilePath.c_str(), targetPath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) success = true; - } else { - if (CopyFileW(g_editState.TempFilePath.c_str(), targetPath.c_str(), FALSE)) { - success = true; - DeleteFileW(g_editState.TempFilePath.c_str()); - } - } - - if (success) { - g_editState.Reset(); - g_imagePath = targetPath; - ReloadCurrentImage(GetActiveWindow()); - } else { - MessageBoxW(nullptr, L"Failed to save file. File locked?", L"Error", MB_ICONERROR); - ReloadCurrentImage(GetActiveWindow()); - return false; - } - return true; -} - -// Helper: Check if file extension matches detected format -bool CheckExtensionMismatch(const std::wstring& path, const std::wstring& format) { - if (path.empty() || format.empty()) return false; - - // Skip .tmp files (temporary files during editing) - std::wstring pathLower = path; - std::transform(pathLower.begin(), pathLower.end(), pathLower.begin(), ::towlower); - if (pathLower.ends_with(L".tmp")) return false; - - size_t lastDot = path.find_last_of(L'.'); - if (lastDot == std::wstring::npos) return true; // No extension is a mismatch (technically) - - std::wstring ext = path.substr(lastDot); - std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); - - std::wstring fmt = format; - std::transform(fmt.begin(), fmt.end(), fmt.begin(), ::towlower); - - // Basic mapping - if (fmt == L"jpeg") return (ext != L".jpg" && ext != L".jpeg" && ext != L".jpe" && ext != L".jfif"); - if (fmt == L"png") return (ext != L".png"); - if (fmt == L"webp") return (ext != L".webp"); - if (fmt == L"avif") return (ext != L".avif"); - if (fmt == L"gif") return (ext != L".gif"); - if (fmt == L"bmp") return (ext != L".bmp" && ext != L".dib"); - if (fmt == L"tiff") return (ext != L".tif" && ext != L".tiff"); - if (fmt == L"heif" || fmt == L"heic") return (ext != L".heic" && ext != L".heif"); - - // JXL - if (fmt == L"jxl" || fmt == L"jpeg xl") return (ext != L".jxl"); - - return false; -} - -void DiscardChanges() { - // Save original path BEFORE reset (Reset clears it) - std::wstring originalPath = g_editState.OriginalFilePath; - - if (g_editState.IsDirty && !g_editState.TempFilePath.empty()) { - ReleaseImageResources(); - if (!DeleteFileW(g_editState.TempFilePath.c_str())) { Sleep(100); DeleteFileW(g_editState.TempFilePath.c_str()); } - } - g_editState.Reset(); - - // Restore to original file path if we had one - if (!originalPath.empty()) { - g_imagePath = originalPath; - ReloadCurrentImage(GetActiveWindow()); - } -} - -bool CheckUnsavedChanges(HWND hwnd) { - if (!g_editState.IsDirty) return true; - if (g_config.ShouldAutoSave(g_editState.Quality)) return SaveCurrentImage(false); - - std::vector buttons = { - { DialogResult::Yes, AppStrings::Dialog_ButtonSave, true }, - { DialogResult::Custom1, AppStrings::Dialog_ButtonSaveAs }, - { DialogResult::No, AppStrings::Dialog_ButtonDiscard } - }; - - const wchar_t* checkboxLabel = AppStrings::Checkbox_AlwaysSaveLossless; - std::wstring qualityMsg = L"Quality: Lossless"; - if (g_editState.Quality == EditQuality::EdgeAdapted) { - checkboxLabel = AppStrings::Checkbox_AlwaysSaveEdgeAdapted; - qualityMsg = L"Quality: Edge Adapted"; - } - else if (g_editState.Quality == EditQuality::Lossy) { - checkboxLabel = AppStrings::Checkbox_AlwaysSaveLossy; - qualityMsg = L"Quality: Lossy Re-encoded"; - } - - DialogResult result = ShowQuickViewDialog(hwnd, AppStrings::Dialog_SaveTitle, AppStrings::Dialog_SaveContent, - g_editState.GetQualityColor(), buttons, true, checkboxLabel, qualityMsg); - - if (result == DialogResult::None) return false; - - if (g_dialog.IsChecked) { - if (g_editState.Quality == EditQuality::EdgeAdapted) g_config.AlwaysSaveEdgeAdapted = true; - else if (g_editState.Quality == EditQuality::Lossy) g_config.AlwaysSaveLossy = true; - else g_config.AlwaysSaveLossless = true; - } - - if (result == DialogResult::Yes) return SaveCurrentImage(false); - if (result == DialogResult::Custom1) return SaveCurrentImage(true); - if (result == DialogResult::No) { DiscardChanges(); return true; } - if (result == DialogResult::Cancel) return false; - - return false; -} - -void AdjustWindowToImage(HWND hwnd) { - if (!g_currentBitmap) return; - if (g_config.LockWindowSize) return; // Don't auto-resize when locked - - D2D1_SIZE_F size = g_currentBitmap->GetSize(); // DIPs - - // Get DPI - float dpi = 96.0f; - UINT dpiX = 96, dpiY = 96; - if (SUCCEEDED(GetDpiForMonitor(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), MDT_EFFECTIVE_DPI, &dpiX, &dpiY))) { - dpi = (float)dpiX; - } - - // Convert to Pixels - int imgW = static_cast(size.width * (dpi / 96.0f)); - int imgH = static_cast(size.height * (dpi / 96.0f)); - - // Add margin for borderless look (optional, but good for shadow) - // Actually our client area IS the window size now in borderless. - - // Get Monitor Work Area - HMONITOR hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; - GetMonitorInfoW(hMon, &mi); - - int maxW = (mi.rcWork.right - mi.rcWork.left) * 9 / 10; - int maxH = (mi.rcWork.bottom - mi.rcWork.top) * 9 / 10; - - int targetW = imgW; - int targetH = imgH; - - // Scale down if too big - if (targetW > maxW || targetH > maxH) { - float ratio = std::min((float)maxW / targetW, (float)maxH / targetH); - targetW = (int)(targetW * ratio); - targetH = (int)(targetH * ratio); - } - - // Minimum size for UI controls - if (targetW < 400) targetW = 400; - if (targetH < 300) targetH = 300; - - // Center logic - RECT rcWindow; GetWindowRect(hwnd, &rcWindow); - int currentCenterX = rcWindow.left + (rcWindow.right - rcWindow.left) / 2; - int currentCenterY = rcWindow.top + (rcWindow.bottom - rcWindow.top) / 2; - - // Using SetWindowPos to resize and center roughly - // Or just resize around center? - int newLeft = currentCenterX - targetW / 2; - int newTop = currentCenterY - targetH / 2; - - // Ensure on screen - if (newLeft < mi.rcWork.left) newLeft = mi.rcWork.left; - if (newTop < mi.rcWork.top) newTop = mi.rcWork.top; - - SetWindowPos(hwnd, nullptr, newLeft, newTop, targetW, targetH, SWP_NOZORDER | SWP_NOACTIVATE); -} - -void ReloadCurrentImage(HWND hwnd) { - if (g_imagePath.empty() && g_editState.OriginalFilePath.empty()) return; - g_currentBitmap.Reset(); - LPCWSTR path; - if (g_editState.IsDirty && FileExists(g_editState.TempFilePath.c_str())) path = g_editState.TempFilePath.c_str(); - else path = g_editState.OriginalFilePath.empty() ? g_imagePath.c_str() : g_editState.OriginalFilePath.c_str(); - - LoadImageAsync(hwnd, path); - // Note: AdjustWindowToImage is called inside LoadImageAsync upon success -} - -void PerformTransform(HWND hwnd, TransformType type) { - if (g_imagePath.empty()) return; - - // Set Wait Cursor - HCURSOR hOldCursor = SetCursor(LoadCursor(nullptr, IDC_WAIT)); - - if (!g_editState.IsDirty && g_editState.OriginalFilePath.empty()) { - g_editState.OriginalFilePath = g_imagePath; - g_editState.TempFilePath = g_imagePath + L".rotating.tmp"; - } - ReleaseImageResources(); - TransformResult result; - LPCWSTR inputPath = (g_editState.IsDirty && FileExists(g_editState.TempFilePath.c_str())) ? g_editState.TempFilePath.c_str() : g_editState.OriginalFilePath.c_str(); - LPCWSTR outputPath = g_editState.TempFilePath.c_str(); - - if (CLosslessTransform::IsJPEG(inputPath)) result = CLosslessTransform::TransformJPEG(inputPath, outputPath, type); - else result = CLosslessTransform::TransformGeneric(inputPath, outputPath, type); - - // Restore Cursor - SetCursor(hOldCursor); - - if (result.Success) { - if (type == TransformType::Rotate90CW) g_editState.TotalRotation = (g_editState.TotalRotation + 90) % 360; - else if (type == TransformType::Rotate90CCW) g_editState.TotalRotation = (g_editState.TotalRotation + 270) % 360; - else if (type == TransformType::Rotate180) g_editState.TotalRotation = (g_editState.TotalRotation + 180) % 360; - else if (type == TransformType::FlipHorizontal) g_editState.FlippedH = !g_editState.FlippedH; - else if (type == TransformType::FlipVertical) g_editState.FlippedV = !g_editState.FlippedV; - - g_editState.Quality = result.Quality; - bool isModified = (g_editState.TotalRotation != 0 || g_editState.FlippedH || g_editState.FlippedV); - bool hasDataLoss = (result.Quality == EditQuality::EdgeAdapted || result.Quality == EditQuality::Lossy); - - if (!isModified && !hasDataLoss) { - g_editState.IsDirty = false; - DeleteFileW(g_editState.TempFilePath.c_str()); - g_osd.Show(hwnd, std::wstring(CLosslessTransform::GetTransformName(type)) + L" (Restored)", false, false, g_editState.GetQualityColor()); - } else { - g_editState.IsDirty = true; - g_osd.Show(hwnd, std::wstring(CLosslessTransform::GetTransformName(type)) + L" - " + g_editState.GetQualityText(), false, false, g_editState.GetQualityColor()); - } - ReloadCurrentImage(hwnd); - } else { - g_osd.Show(hwnd, std::wstring(CLosslessTransform::GetTransformName(type)) + L" failed: " + result.ErrorMessage, true); - ReloadCurrentImage(hwnd); - } -} - -int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { - // Enable Per-Monitor DPI Awareness V2 for proper multi-monitor support - // This enables WM_DPICHANGED messages when window is dragged across monitors - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - - HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - WNDCLASSEXW wcex = {}; - wcex.cbSize = sizeof(WNDCLASSEXW); - wcex.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; - wcex.lpfnWndProc = WndProc; - wcex.hInstance = hInstance; - wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); - wcex.hbrBackground = nullptr; - wcex.lpszClassName = g_szClassName; - wcex.hIcon = LoadIconW(hInstance, MAKEINTRESOURCEW(1)); // Load from resource - wcex.hIconSm = LoadIconW(hInstance, MAKEINTRESOURCEW(1)); // Load from resource - - RegisterClassExW(&wcex); - HWND hwnd = CreateWindowExW(0, g_szClassName, g_szWindowTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr); - if (!hwnd) return 0; - - g_renderEngine = std::make_unique(); g_renderEngine->Initialize(hwnd); - g_imageLoader = std::make_unique(); g_imageLoader->Initialize(g_renderEngine->GetWICFactory()); - - // Init Gallery - g_thumbMgr.Initialize(hwnd, g_imageLoader.get()); - g_gallery.Initialize(&g_thumbMgr, &g_navigator); - DragAcceptFiles(hwnd, TRUE); - ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); - - int argc = 0; LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); - if (argc > 1) { - g_navigator.Initialize(argv[1]); - LoadImageAsync(hwnd, argv[1]); - } else { - // No file specified - auto open file dialog - OPENFILENAMEW ofn = {}; - wchar_t szFile[MAX_PATH] = {}; - ofn.lStructSize = sizeof(ofn); - ofn.hwndOwner = hwnd; - ofn.lpstrFile = szFile; - ofn.nMaxFile = MAX_PATH; - ofn.lpstrFilter = L"All Images\0*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.webp;*.avif;*.jxl;*.heic;*.heif;*.tga;*.psd;*.hdr;*.exr;*.svg;*.qoi;*.pcx;*.raw;*.arw;*.cr2;*.cr3;*.nef;*.dng;*.orf;*.rw2;*.raf;*.pef;*.pgm;*.ppm\0JPEG\0*.jpg;*.jpeg\0PNG\0*.png\0WebP\0*.webp\0AVIF\0*.avif\0JPEG XL\0*.jxl\0HEIC/HEIF\0*.heic;*.heif\0RAW\0*.raw;*.arw;*.cr2;*.cr3;*.nef;*.dng;*.orf;*.rw2;*.raf;*.pef\0All Files\0*.*\0"; - ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; - if (GetOpenFileNameW(&ofn)) { - g_navigator.Initialize(szFile); - LoadImageAsync(hwnd, szFile); - } - } - LocalFree(argv); - - MSG msg; - while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } - DiscardChanges(); CoUninitialize(); return (int)msg.wParam; -} - -LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { - static bool isTracking = false; - switch (message) { - case WM_CREATE: { - MARGINS margins = { 0, 0, 0, 1 }; - DwmExtendFrameIntoClientArea(hwnd, &margins); - SetWindowPos(hwnd, nullptr, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER); - return 0; - } - case WM_NCCALCSIZE: if (wParam) return 0; break; - case WM_APP + 1: { - auto handle = std::coroutine_handle<>::from_address((void*)lParam); - handle.resume(); - return 0; - } - case WM_TIMER: { - static const UINT_PTR INTERACTION_TIMER_ID = 1001; - static const UINT_PTR OSD_TIMER_ID = 999; - - // Interaction Timer (1001) - if (wParam == INTERACTION_TIMER_ID) { - KillTimer(hwnd, INTERACTION_TIMER_ID); - g_viewState.IsInteracting = false; // End interaction mode - InvalidateRect(hwnd, nullptr, FALSE); // Redraw with high quality - } - - // OSD Timer (999) - Heartbeat/Expiration check - if (wParam == OSD_TIMER_ID) { - if (!g_osd.IsVisible()) { - KillTimer(hwnd, OSD_TIMER_ID); - InvalidateRect(hwnd, nullptr, FALSE); // Clear it (redraw without OSD) - } - } - - // Gallery Fade Timer (998) - if (wParam == 998) { - if (g_gallery.IsVisible()) { - InvalidateRect(hwnd, nullptr, FALSE); - // Stop timer if opacity reached full - if (g_gallery.GetOpacity() >= 1.0f) { - KillTimer(hwnd, 998); - } - } else { - KillTimer(hwnd, 998); - } - } - - // Toolbar Animation Timer (997) - if (wParam == 997) { - // Auto-Hide Delay Logic - if (g_toolbarHideTime > 0 && (GetTickCount() - g_toolbarHideTime > 2000)) { - if (!g_toolbar.IsPinned()) g_toolbar.SetVisible(false); - } - - bool animating = g_toolbar.UpdateAnimation(); - - // Keep timer alive if motivating factor exists (Animation OR Pending Hide) - if (animating || (g_toolbar.IsVisible() && !g_toolbar.IsPinned() && g_toolbarHideTime > 0)) { - InvalidateRect(hwnd, nullptr, FALSE); - } else { - KillTimer(hwnd, 997); - } - } - return 0; - } - case WM_NCHITTEST: { - LRESULT hit = DefWindowProc(hwnd, message, wParam, lParam); - if (hit != HTCLIENT) return hit; - - POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - RECT rc; GetWindowRect(hwnd, &rc); - int border = 8; - - if (pt.y < rc.top + border) { - if (pt.x < rc.left + border) return HTTOPLEFT; - if (pt.x > rc.right - border) return HTTOPRIGHT; - return HTTOP; - } - if (pt.y > rc.bottom - border) { - if (pt.x < rc.left + border) return HTBOTTOMLEFT; - if (pt.x > rc.right - border) return HTBOTTOMRIGHT; - return HTBOTTOM; - } - if (pt.x < rc.left + border) return HTLEFT; - if (pt.x > rc.right - border) return HTRIGHT; - - ScreenToClient(hwnd, &pt); - D2D1_POINT_2F localPt = D2D1::Point2F((float)pt.x, (float)pt.y); - - // Buttons always HTCLIENT (handled in LBUTTONUP/DOWN) - - return HTCLIENT; - } - - case WM_SIZE: - if (wParam != SIZE_MINIMIZED) { - OnResize(hwnd, LOWORD(lParam), HIWORD(lParam)); - CalculateWindowControls(D2D1::SizeF((float)LOWORD(lParam), (float)HIWORD(lParam))); - } - return 0; - - case WM_DPICHANGED: { - // Handle DPI change (e.g., window dragged to different monitor) - // wParam: LOWORD = new X DPI, HIWORD = new Y DPI - // lParam: pointer to RECT with suggested new window size/position - RECT* pNewRect = (RECT*)lParam; - SetWindowPos(hwnd, nullptr, - pNewRect->left, pNewRect->top, - pNewRect->right - pNewRect->left, - pNewRect->bottom - pNewRect->top, - SWP_NOZORDER | SWP_NOACTIVATE); - // WM_SIZE will be triggered by SetWindowPos, which calls OnResize - // No additional action needed - D2D handles DPI internally - return 0; - } - - case WM_CLOSE: if (!CheckUnsavedChanges(hwnd)) return 0; DestroyWindow(hwnd); return 0; - case WM_DESTROY: g_thumbMgr.Shutdown(); PostQuitMessage(0); return 0; - - // Mouse Interaction - case WM_MOUSEMOVE: { - POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - - if (g_gallery.IsVisible()) { - - g_gallery.OnMouseMove((float)pt.x, (float)pt.y); - InvalidateRect(hwnd, nullptr, FALSE); // Always redraw for tooltip - } - - // Toolbar Trigger - RECT rc; GetClientRect(hwnd, &rc); - float winH = (float)(rc.bottom - rc.top); - float zoneHeight = g_toolbar.IsVisible() ? 100.0f : 60.0f; // Expanded zone if visible - bool inZone = (pt.y > winH - zoneHeight); - - static DWORD s_hideRequestTime = 0; - - if (inZone || g_toolbar.IsPinned()) { - g_toolbar.SetVisible(true); - s_hideRequestTime = 0; - } else { - // Outside zone and not pinned - if (g_toolbar.IsVisible() && s_hideRequestTime == 0) { - s_hideRequestTime = GetTickCount(); // Start countdown - } - } - - // Pass intent to Timer: - // We can't pass 's_hideRequestTime' to Timer easily without global. - // Let's use a static variable in main.cpp scope or just a global. - // For now, let's just use SetVisible(false) here IF the delay was short, but for 2s delay we need Timer. - // Let's store s_hideRequestTime in a global "g_toolbarHideTime" for simplicity. - extern DWORD g_toolbarHideTime; // Defined in global scope - g_toolbarHideTime = s_hideRequestTime; - - g_toolbar.OnMouseMove((float)pt.x, (float)pt.y); - - // Set hand cursor when hovering toolbar buttons - if (g_toolbar.IsVisible() && g_toolbar.HitTest((float)pt.x, (float)pt.y)) { - SetCursor(LoadCursor(nullptr, IDC_HAND)); - } - - SetTimer(hwnd, 997, 16, nullptr); // Drive animation/latency logic - // Force redraw for hover effects when toolbar is visible - if (g_toolbar.IsVisible() || g_config.ShowInfoPanel) { - InvalidateRect(hwnd, nullptr, FALSE); - } - // Update Button Hover - WindowHit oldHit = g_winControls.HoverState; - g_winControls.HoverState = WindowHit::None; - - // Auto-Show Controls Logic - RECT rcClient; GetClientRect(hwnd, &rcClient); - bool inTopArea = (pt.y <= 60); // 60px top area - - if (g_config.AutoHideWindowControls) { - bool shouldShow = inTopArea || (oldHit != WindowHit::None); // Keep showing if previously hovering button? - // Simpler: Just rely on mouse Y. - if (inTopArea != g_showControls) { - g_showControls = inTopArea; - InvalidateRect(hwnd, nullptr, FALSE); - } - } else { - if (!g_showControls) { g_showControls = true; InvalidateRect(hwnd, nullptr, FALSE); } - } - - if (g_showControls) { - if (pt.x >= (long)g_winControls.CloseRect.left && pt.x <= (long)g_winControls.CloseRect.right && pt.y <= (long)g_winControls.CloseRect.bottom) - g_winControls.HoverState = WindowHit::Close; - else if (pt.x >= (long)g_winControls.MaxRect.left && pt.x <= (long)g_winControls.MaxRect.right && pt.y <= (long)g_winControls.MaxRect.bottom) - g_winControls.HoverState = WindowHit::Max; - else if (pt.x >= (long)g_winControls.MinRect.left && pt.x <= (long)g_winControls.MinRect.right && pt.y <= (long)g_winControls.MinRect.bottom) - g_winControls.HoverState = WindowHit::Min; - else if (pt.x >= (long)g_winControls.PinRect.left && pt.x <= (long)g_winControls.PinRect.right && pt.y <= (long)g_winControls.PinRect.bottom) - g_winControls.HoverState = WindowHit::Pin; - - // Hand cursor for window control buttons - if (g_winControls.HoverState != WindowHit::None) { - SetCursor(LoadCursor(nullptr, IDC_HAND)); - } - } - - if (oldHit != g_winControls.HoverState) { - if (!isTracking) { - TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT), TME_LEAVE, hwnd, 0 }; - TrackMouseEvent(&tme); - isTracking = true; - } - InvalidateRect(hwnd, nullptr, FALSE); - } - - if (g_viewState.IsDragging) { - g_viewState.PanX += (pt.x - g_viewState.LastMousePos.x); - g_viewState.PanY += (pt.y - g_viewState.LastMousePos.y); - g_viewState.LastMousePos = pt; - InvalidateRect(hwnd, nullptr, FALSE); - } - - // Hand cursor for info panel clickable areas - if (g_config.ShowInfoPanel) { - float mx = (float)pt.x, my = (float)pt.y; - bool onClickable = false; - - // Toggle/Close buttons - if ((mx >= g_panelToggleRect.left && mx <= g_panelToggleRect.right && my >= g_panelToggleRect.top && my <= g_panelToggleRect.bottom) || - (mx >= g_panelCloseRect.left && mx <= g_panelCloseRect.right && my >= g_panelCloseRect.top && my <= g_panelCloseRect.bottom)) { - onClickable = true; - } - - // Info grid rows (when expanded) - if (g_config.InfoPanelExpanded) { - for (const auto& row : g_infoGrid) { - if (mx >= row.hitRect.left && mx <= row.hitRect.right && my >= row.hitRect.top && my <= row.hitRect.bottom) { - onClickable = true; break; - } - } - // GPS coordinate and link - if (g_currentMetadata.HasGPS) { - if ((mx >= g_gpsCoordRect.left && mx <= g_gpsCoordRect.right && my >= g_gpsCoordRect.top && my <= g_gpsCoordRect.bottom) || - (mx >= g_gpsLinkRect.left && mx <= g_gpsLinkRect.right && my >= g_gpsLinkRect.top && my <= g_gpsLinkRect.bottom)) { - onClickable = true; - } - } - } - - if (onClickable) SetCursor(LoadCursor(nullptr, IDC_HAND)); - } - return 0; - } - case WM_MOUSELEAVE: - g_winControls.HoverState = WindowHit::None; - if (g_config.AutoHideWindowControls) { g_showControls = false; } - isTracking = false; - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - - - - case WM_MBUTTONDOWN: { - // Record start position/time for click vs drag detection - g_viewState.LastMousePos = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - g_viewState.DragStartPos = g_viewState.LastMousePos; - g_viewState.DragStartTime = GetTickCount(); - - // Only allow panning if image exceeds window bounds - if (CanPan(hwnd)) { - SetCapture(hwnd); - g_viewState.IsDragging = true; - g_viewState.IsInteracting = true; // Start interaction mode - } else { - // Not dragging, but still need to track for click detection - g_viewState.IsDragging = false; // Will check on MBUTTONUP - } - return 0; - } - case WM_MBUTTONUP: { - // Release capture if we were dragging - if (g_viewState.IsDragging) { - ReleaseCapture(); - g_viewState.IsDragging = false; - } - - // FIRST: Check if this was a "click" (short duration, minimal movement) - // Must check BEFORE setting IsInteracting=false to avoid triggering HIGH_QUALITY repaint - POINT currentPos = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - DWORD elapsed = GetTickCount() - g_viewState.DragStartTime; - int dx = abs(currentPos.x - g_viewState.DragStartPos.x); - int dy = abs(currentPos.y - g_viewState.DragStartPos.y); - - // Click threshold: <300ms and <5px movement - if (elapsed < 300 && dx < 5 && dy < 5) { - if (g_config.MiddleClickAction == MouseAction::ExitApp) { - // CRITICAL FIX: Use WM_CLOSE instead of PostQuitMessage - // PostQuitMessage kills the message loop immediately, causing DXGI - // cleanup deadlock (SwapChain needs to communicate with HWND during Release) - // WM_CLOSE triggers proper destruction: WM_CLOSE -> DestroyWindow -> WM_DESTROY - // This allows DXGI to clean up while message loop is still active - if (CheckUnsavedChanges(hwnd)) { - PostMessage(hwnd, WM_CLOSE, 0, 0); - return 0; - } - } - } - - // Only end interaction and repaint if NOT exiting - g_viewState.IsInteracting = false; - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - } - - case WM_LBUTTONDBLCLK: - // Fit Window - restore from maximized first if needed - if (IsZoomed(hwnd)) { - ShowWindow(hwnd, SW_RESTORE); - } - g_viewState.Reset(); - AdjustWindowToImage(hwnd); // Reset window size too - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - - case WM_PAINT: - OnPaint(hwnd); - // Gallery Animation Loop if visible - if (g_gallery.IsVisible()) { - // Need continuous update for fade-in? - // Simple hack: Invalidate if fading - // Or use OnPaint delta time? - // Let's assume OnPaint happens? - // Better: SetTimer logic or just Invalidate if opacity < 1.0f inside Render? - // GalleryOverlay::Render doesn't invalidate. - // Let's rely on standard Paint messages or manual Invalidate from Interaction. - // For animation "Fade In", we should probably Invalidate continuously for 0.2s. - // We can add logic in OnPaint or just Timer? - // Simplest: In Render call, if animating, POST Invalidate? - // Let's do nothing special here, OnPaint calls Render. - } - return 0; - - case WM_THUMB_KEY_READY: - // Redraw only if gallery visible - if (g_gallery.IsVisible()) { - InvalidateRect(hwnd, nullptr, FALSE); - } - return 0; - - case WM_LBUTTONDOWN: { - POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - - // Window control buttons click handling - if (g_showControls && g_winControls.HoverState != WindowHit::None) { - switch (g_winControls.HoverState) { - case WindowHit::Close: SendMessage(hwnd, WM_CLOSE, 0, 0); return 0; - case WindowHit::Max: ShowWindow(hwnd, IsZoomed(hwnd) ? SW_RESTORE : SW_MAXIMIZE); return 0; - case WindowHit::Min: ShowWindow(hwnd, SW_MINIMIZE); return 0; - case WindowHit::Pin: SendMessage(hwnd, WM_COMMAND, IDM_ALWAYS_ON_TOP, 0); return 0; - default: break; - } - } - - if (g_gallery.IsVisible()) { - if (g_gallery.OnLButtonDown(pt.x, pt.y)) { - // Check if closed with selection - if (!g_gallery.IsVisible()) { - int idx = g_gallery.GetSelectedIndex(); - if (idx >= 0 && idx < (int)g_navigator.Count()) { - // Sync Navigator? - // Navigator doesn't have SetIndex. Need to use file path. - std::wstring path = g_navigator.GetFile(idx); - g_navigator.Initialize(path); // Re-init to jump to index? (Inefficient but robust) - // Actually Initialize does scanning. FindIndex is better but we don't have SetIndex. - // Let's just LoadImageAsync(path). Navigator usually updates on Load? - // LoadImageAsync helper doesn't update Navigator index explicitly unless it scans? - // Initialize updates index. - // But if we just call LoadImageAsync(hwnd, path), DragDrop does Initialize. - // Let's re-initialize navigator carefully or just assume path loading works. - // Correct flow: - g_navigator.Initialize(path); // Update index state - LoadImageAsync(hwnd, path.c_str()); - } - InvalidateRect(hwnd, nullptr, FALSE); - } else { - InvalidateRect(hwnd, nullptr, FALSE); - } - } - return 0; - } - - if (g_config.ShowInfoPanel) { - // 1. Toggle Button - if (pt.x >= g_panelToggleRect.left && pt.x <= g_panelToggleRect.right && - pt.y >= g_panelToggleRect.top && pt.y <= g_panelToggleRect.bottom) { - g_config.InfoPanelExpanded = !g_config.InfoPanelExpanded; - // Lazy Load Histogram if expanding and data missing - if (g_config.InfoPanelExpanded && g_currentMetadata.HistR.empty() && !g_imagePath.empty()) { - UpdateHistogramAsync(hwnd, g_imagePath); - } - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - } - - // 2. Close Button - if (pt.x >= g_panelCloseRect.left && pt.x <= g_panelCloseRect.right && - pt.y >= g_panelCloseRect.top && pt.y <= g_panelCloseRect.bottom) { - g_config.ShowInfoPanel = false; - g_toolbar.SetExifState(false); // Sync toolbar icon state - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - } - - // 3. Grid Row Click (Copy to Clipboard) - if (g_config.InfoPanelExpanded) { - float mx = (float)pt.x; - float my = (float)pt.y; - for (const auto& row : g_infoGrid) { - if (mx >= row.hitRect.left && mx <= row.hitRect.right && - my >= row.hitRect.top && my <= row.hitRect.bottom) { - - std::wstring textToCopy = row.fullText.empty() ? row.valueMain : row.valueMain; // Prefer Main or Full? - // Logic: If truncated, fullText usually holds untruncated. If unique (like filename), fullText holds basename. - // Let's use fullText if available and different? - // Actually: - // File: fullText = basename - // Size: fullText = "" - // Lens: fullText = lens name - // So fullText is good. If empty, use valueMain. - if (!row.fullText.empty()) textToCopy = row.fullText; - else textToCopy = row.valueMain; - - // Special case: File -> Full Path - if (row.label == L"File") textToCopy = g_imagePath; - - if (CopyToClipboard(hwnd, textToCopy)) { - g_osd.Show(hwnd, L"Copied!", false); - } - InvalidateRect(hwnd, nullptr, FALSE); // For visual feedback if we add click effect later - return 0; - } - } - } - - // 3. GPS Coordinates (Click to Copy) - if (g_config.InfoPanelExpanded && g_currentMetadata.HasGPS) { - if (pt.x >= g_gpsCoordRect.left && pt.x <= g_gpsCoordRect.right && - pt.y >= g_gpsCoordRect.top && pt.y <= g_gpsCoordRect.bottom) { - wchar_t coordBuf[128]; - swprintf_s(coordBuf, L"%.6f, %.6f", g_currentMetadata.Latitude, g_currentMetadata.Longitude); - if (CopyToClipboard(hwnd, coordBuf)) { - g_osd.Show(hwnd, L"Coordinates copied!", false); - } - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - } - } - - // 4. GPS Link (Bing Maps) - Only if Expanded - if (g_config.InfoPanelExpanded && g_currentMetadata.HasGPS) { - if (pt.x >= g_gpsLinkRect.left && pt.x <= g_gpsLinkRect.right && - pt.y >= g_gpsLinkRect.top && pt.y <= g_gpsLinkRect.bottom) { - wchar_t url[256]; - // Bing Maps URL format - swprintf_s(url, L"https://www.bing.com/maps?where1=%.6f%%2C%.6f", g_currentMetadata.Latitude, g_currentMetadata.Longitude); - ShellExecuteW(nullptr, L"open", url, nullptr, nullptr, SW_SHOWNORMAL); - return 0; - } - } - - // 5. Filename (Click to Copy) - Only if Expanded - if (g_config.InfoPanelExpanded) { - if (pt.x >= g_filenameRect.left && pt.x <= g_filenameRect.right && - pt.y >= g_filenameRect.top && pt.y <= g_filenameRect.bottom) { - std::wstring filename = g_imagePath.substr(g_imagePath.find_last_of(L"\\/") + 1); - if (CopyToClipboard(hwnd, filename)) { - g_osd.Show(hwnd, L"Filename copied!", false); - } - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - } - } - } - - // Buttons (Window Controls) - if (g_showControls) { - if (g_winControls.HoverState == WindowHit::Close) { PostMessage(hwnd, WM_CLOSE, 0, 0); return 0; } - if (g_winControls.HoverState == WindowHit::Max) { - if (IsZoomed(hwnd)) ShowWindow(hwnd, SW_RESTORE); else ShowWindow(hwnd, SW_MAXIMIZE); - return 0; - } - if (g_winControls.HoverState == WindowHit::Min) { ShowWindow(hwnd, SW_MINIMIZE); return 0; } - } - - // Toolbar Interaction - Prevent Window Drag if clicking toolbar - if (g_toolbar.IsVisible() && g_toolbar.HitTest((float)pt.x, (float)pt.y)) { - return 0; // Handled by LBUTTONUP - } - - if (g_config.LeftDragAction == MouseAction::WindowDrag) { - ReleaseCapture(); - SendMessage(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); - return 0; - } else if (g_config.LeftDragAction == MouseAction::PanImage) { - // Only allow panning if image exceeds window bounds - if (CanPan(hwnd)) { - SetCapture(hwnd); - g_viewState.IsDragging = true; - g_viewState.IsInteracting = true; // Start interaction mode - g_viewState.LastMousePos = pt; - } - } - return 0; - } - case WM_LBUTTONUP: { - POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - - // Toolbar Click - ToolbarButtonID tbId; - if (g_toolbar.OnClick((float)pt.x, (float)pt.y, tbId)) { - switch (tbId) { - case ToolbarButtonID::Prev: Navigate(hwnd, -1); break; - case ToolbarButtonID::Next: Navigate(hwnd, 1); break; - case ToolbarButtonID::RotateL: PerformTransform(hwnd, TransformType::Rotate90CCW); break; - case ToolbarButtonID::RotateR: PerformTransform(hwnd, TransformType::Rotate90CW); break; - case ToolbarButtonID::FlipH: PerformTransform(hwnd, TransformType::FlipHorizontal); break; - case ToolbarButtonID::LockSize: SendMessage(hwnd, WM_COMMAND, IDM_LOCK_WINDOW_SIZE, 0); break; - case ToolbarButtonID::Exif: SendMessage(hwnd, WM_COMMAND, IDM_SHOW_INFO_PANEL, 0); break; - case ToolbarButtonID::RawToggle: { - g_config.ForceRawDecode = !g_config.ForceRawDecode; - // Reload - ReleaseImageResources(); // Free current - LoadImageAsync(hwnd, g_imagePath); - - std::wstring msg = g_config.ForceRawDecode ? L"RAW: Full Decode (High Quality)" : L"RAW: Embedded Preview (Fast)"; - g_osd.Show(hwnd, msg, false); - break; - } - case ToolbarButtonID::FixExtension: SendMessage(hwnd, WM_COMMAND, IDM_FIX_EXTENSION, 0); break; - case ToolbarButtonID::Pin: { - g_toolbar.TogglePin(); - // Refresh layout to update icon - RECT rc; GetClientRect(hwnd, &rc); - g_toolbar.UpdateLayout((float)rc.right, (float)rc.bottom); - InvalidateRect(hwnd, nullptr, FALSE); - break; - } - case ToolbarButtonID::Gallery: - if (g_gallery.IsVisible()) g_gallery.Close(); else g_gallery.Open(g_navigator.Index()); - InvalidateRect(hwnd, nullptr, FALSE); - break; - } - return 0; - } - - if (g_viewState.IsDragging) { - ReleaseCapture(); - g_viewState.IsDragging = false; - } - g_viewState.IsInteracting = false; // End interaction mode - InvalidateRect(hwnd, nullptr, FALSE); // Redraw with high quality - return 0; - } - - case WM_MOUSEWHEEL: { - if (g_gallery.IsVisible()) { - int delta = GET_WHEEL_DELTA_WPARAM(wParam); - if (g_gallery.OnMouseWheel(delta)) { - InvalidateRect(hwnd, nullptr, FALSE); - } - return 0; - } - if (!g_currentBitmap) return 0; - - // --- Magnetic Snap Time Lock --- - static DWORD s_lastSnapTime = 0; - if (GetTickCount() - s_lastSnapTime < 400) { - return 0; // Ignore input briefly after snapping - } - - // Enable interaction mode during zoom (use LINEAR interpolation) - g_viewState.IsInteracting = true; - // Set timer to reset interaction mode after 150ms of inactivity - static const UINT_PTR INTERACTION_TIMER_ID = 1001; - SetTimer(hwnd, INTERACTION_TIMER_ID, 150, nullptr); - - float delta = GET_WHEEL_DELTA_WPARAM(wParam) / 120.0f; - - // Calc Current Total Scale (Fit * Zoom) - RECT rc; GetClientRect(hwnd, &rc); - D2D1_SIZE_F imgSize = g_currentBitmap->GetSize(); - float fitScale = std::min((float)rc.right / imgSize.width, (float)rc.bottom / imgSize.height); - float currentTotalScale = fitScale * g_viewState.Zoom; - - float zoomFactor = (delta > 0) ? 1.1f : 0.90909f; - float newTotalScale = currentTotalScale * zoomFactor; - - // --- Magnetic Snap to 100% --- - const float SNAP_THRESHOLD = 0.05f; - bool isAlreadyAt100 = (abs(currentTotalScale - 1.0f) < 0.001f); - - if (!isAlreadyAt100) { - bool snapped = false; - // Check for crossing 1.0 - if ((currentTotalScale < 1.0f && newTotalScale > 1.0f) || - (currentTotalScale > 1.0f && newTotalScale < 1.0f)) { - newTotalScale = 1.0f; - snapped = true; - } - // Check for proximity - else if (abs(newTotalScale - 1.0f) < SNAP_THRESHOLD) { - newTotalScale = 1.0f; - snapped = true; - } - - if (snapped) { - s_lastSnapTime = GetTickCount(); // Engage Time Lock - } - } - - // Limits - if (newTotalScale < 0.1f * fitScale) newTotalScale = 0.1f * fitScale; // Min 10% of FIT - if (newTotalScale > 20.0f) newTotalScale = 20.0f; - - if (g_config.ResizeWindowOnZoom && !IsZoomed(hwnd) && !g_config.LockWindowSize) { - // Calculate Desired Window Size to achieve NewTotalScale with Zoom=1.0 (Fit) - // DesiredCanvasW = ImageW * NewTotalScale - // But FitScale logic is Window / Image. - // If we set Window = Image * NewTotalScale, then FitScale = NewTotalScale. - // So ViewState.Zoom should be 1.0. - - // Get Monitor Info - HMONITOR hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; GetMonitorInfoW(hMon, &mi); - int maxW = (mi.rcWork.right - mi.rcWork.left); - int maxH = (mi.rcWork.bottom - mi.rcWork.top); - - int targetW = (int)(imgSize.width * newTotalScale); - int targetH = (int)(imgSize.height * newTotalScale); - - // Clamp - bool capped = false; - if (targetW > maxW) { targetW = maxW; capped = true; } - if (targetH > maxH) { targetH = maxH; capped = true; } - if (targetW < 400) { targetW = 400; capped = true; } // Min size - if (targetH < 300) { targetH = 300; capped = true; } - - // Apply Window Resize - // Center window on screen or keep center? - // Usually keep center. - RECT rcWin; GetWindowRect(hwnd, &rcWin); - int cX = rcWin.left + (rcWin.right - rcWin.left) / 2; - int cY = rcWin.top + (rcWin.bottom - rcWin.top) / 2; - - // If we are resizing window, 'FitScale' changes. - // Final 'FitScale' will be min(TargetW/ImgW, TargetH/ImgH). - // We want FinalTotalScale = NewTotalScale. - // So ViewState.Zoom = NewTotalScale / FinalFitScale. - - SetWindowPos(hwnd, nullptr, cX - targetW/2, cY - targetH/2, targetW, targetH, SWP_NOZORDER | SWP_NOACTIVATE); - - // Recalculate Zoom - float newFitScale = std::min((float)targetW / imgSize.width, (float)targetH / imgSize.height); - g_viewState.Zoom = newTotalScale / newFitScale; - - // Reset Pan when window adapting (optional, keeps image centered) - if (!capped) { g_viewState.PanX = 0; g_viewState.PanY = 0; } - } else { - // Standard Zoom (Window size fixed or Maxed) - // Zoom centered on window center - RECT rcNew; GetClientRect(hwnd, &rcNew); - - float newFitScale = std::min((float)rcNew.right / imgSize.width, (float)rcNew.bottom / imgSize.height); - float oldZoom = g_viewState.Zoom; - float newZoom = newTotalScale / newFitScale; - - // Simple center-based zoom: just scale Pan values proportionally - // Since zoom is centered on window center, pan should scale with zoom ratio - float zoomRatio = newZoom / oldZoom; - g_viewState.PanX *= zoomRatio; - g_viewState.PanY *= zoomRatio; - g_viewState.Zoom = newZoom; - } - - // Show Zoom OSD - int percent = (int)(std::round(newTotalScale * 100.0f)); - bool is100 = (abs(newTotalScale - 1.0f) < 0.001f); - - wchar_t zoomBuf[32]; - swprintf_s(zoomBuf, L"Zoom: %d%%", percent); - D2D1_COLOR_F color = is100 ? D2D1::ColorF(0.4f, 1.0f, 0.4f) : D2D1::ColorF(D2D1::ColorF::White); // Green if 100% - - g_osd.Show(hwnd, zoomBuf, false, false, color); - - InvalidateRect(hwnd, nullptr, FALSE); - return 0; - } - - - case WM_DROPFILES: { - if (!CheckUnsavedChanges(hwnd)) return 0; - HDROP hDrop = reinterpret_cast(wParam); - wchar_t path[MAX_PATH]; - if (DragQueryFileW(hDrop, 0, path, MAX_PATH)) { - g_editState.Reset(); - g_viewState.Reset(); - g_navigator.Initialize(path); - g_thumbMgr.ClearCache(); // Fix: Clear old thumbnails on folder switch - LoadImageAsync(hwnd, path); // Async - InvalidateRect(hwnd, nullptr, FALSE); - } - DragFinish(hDrop); - return 0; - } - case WM_KEYDOWN: { - // Gallery handling - if (g_gallery.IsVisible()) { - if (g_gallery.OnKeyDown(wParam)) { - if (!g_gallery.IsVisible()) { - // Closed with selection potentially - int idx = g_gallery.GetSelectedIndex(); - if (idx >= 0 && idx < (int)g_navigator.Count()) { - std::wstring path = g_navigator.GetFile(idx); - g_navigator.Initialize(path); - LoadImageAsync(hwnd, path.c_str()); - } - InvalidateRect(hwnd, nullptr, FALSE); - } else { - InvalidateRect(hwnd, nullptr, FALSE); - } - return 0; - } - // If ESC handled by gallery, fine. - } else { - // Not Visible - Handled in switch below - } - - bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0; - bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0; - bool alt = (GetKeyState(VK_MENU) & 0x8000) != 0; - - switch (wParam) { - // Navigation - case VK_LEFT: Navigate(hwnd, -1); break; - case VK_RIGHT: Navigate(hwnd, 1); break; - case VK_SPACE: Navigate(hwnd, 1); break; - - // File operations - case 'O': SendMessage(hwnd, WM_COMMAND, IDM_OPEN, 0); break; // O or Ctrl+O: Open - case 'E': SendMessage(hwnd, WM_COMMAND, IDM_EDIT, 0); break; // E: Edit - case VK_F2: SendMessage(hwnd, WM_COMMAND, IDM_RENAME, 0); break; // F2: Rename - case VK_DELETE: SendMessage(hwnd, WM_COMMAND, IDM_DELETE, 0); break; // Del: Delete - case 'P': if (ctrl) { SendMessage(hwnd, WM_COMMAND, IDM_PRINT, 0); } break; // Ctrl+P: Print - case 'C': // Ctrl+C: Copy image, Ctrl+Alt+C: Copy path - if (ctrl && alt) { - if (!g_imagePath.empty() && CopyToClipboard(hwnd, g_imagePath)) { - g_osd.Show(hwnd, L"File path copied!", false); - } - } else if (ctrl) { - SendMessage(hwnd, WM_COMMAND, IDM_COPY_IMAGE, 0); - } - break; - - // View - case 'T': // T: Gallery (non-Ctrl), Ctrl+T: Always on Top - if (ctrl) { - SendMessage(hwnd, WM_COMMAND, IDM_ALWAYS_ON_TOP, 0); - } else { - // Toggle Gallery (Only if not visible, ESC closes it) - if (!g_gallery.IsVisible()) { - g_gallery.Open(g_navigator.Index()); - InvalidateRect(hwnd, nullptr, FALSE); - SetTimer(hwnd, 998, 16, nullptr); // Fade in - } - } - break; - case VK_TAB: // Tab: Toggle compact info panel - if (!g_config.ShowInfoPanel) { - g_config.ShowInfoPanel = true; - g_config.InfoPanelExpanded = false; - g_toolbar.SetExifState(true); - } else if (g_config.InfoPanelExpanded) { - g_config.InfoPanelExpanded = false; - } else { - g_config.ShowInfoPanel = false; - g_toolbar.SetExifState(false); - } - InvalidateRect(hwnd, nullptr, FALSE); - break; - case 'I': // I: Toggle full info panel - if (!g_config.ShowInfoPanel) { - g_config.ShowInfoPanel = true; - g_config.InfoPanelExpanded = true; - if (g_currentMetadata.HistR.empty() && !g_imagePath.empty()) { - UpdateHistogramAsync(hwnd, g_imagePath); - } - g_toolbar.SetExifState(true); - } else if (!g_config.InfoPanelExpanded) { - g_config.InfoPanelExpanded = true; - if (g_currentMetadata.HistR.empty() && !g_imagePath.empty()) { - UpdateHistogramAsync(hwnd, g_imagePath); - } - } else { - g_config.ShowInfoPanel = false; - g_toolbar.SetExifState(false); - } - InvalidateRect(hwnd, nullptr, FALSE); - break; - - // Transforms - case 'R': PerformTransform(hwnd, shift ? TransformType::Rotate90CCW : TransformType::Rotate90CW); break; - case 'H': PerformTransform(hwnd, TransformType::FlipHorizontal); break; - case 'V': PerformTransform(hwnd, TransformType::FlipVertical); break; - - // Zoom - // Zoom - // Zoom - case '1': case 'Z': case VK_NUMPAD1: // 100% Original size - if (g_currentBitmap) { - D2D1_SIZE_F imgSize = g_currentBitmap->GetSize(); - if (imgSize.width > 0 && imgSize.height > 0) { - // Logic to resize window to wrap image at 100% if allowed - if (g_config.ResizeWindowOnZoom && !IsZoomed(hwnd) && !g_config.LockWindowSize) { - // Target is 100% of image size - int targetW = (int)imgSize.width; - int targetH = (int)imgSize.height; - - // Get Monitor Info - HMONITOR hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; GetMonitorInfoW(hMon, &mi); - int maxW = (mi.rcWork.right - mi.rcWork.left); - int maxH = (mi.rcWork.bottom - mi.rcWork.top); - - // Clamp to screen - if (targetW > maxW) targetW = maxW; - if (targetH > maxH) targetH = maxH; - // Min size safety - if (targetW < 400) targetW = 400; - if (targetH < 300) targetH = 300; - - // Center Window - RECT rcWin; GetWindowRect(hwnd, &rcWin); - int cX = rcWin.left + (rcWin.right - rcWin.left) / 2; - int cY = rcWin.top + (rcWin.bottom - rcWin.top) / 2; - - SetWindowPos(hwnd, nullptr, cX - targetW/2, cY - targetH/2, targetW, targetH, SWP_NOZORDER | SWP_NOACTIVATE); - - // Recalculate Fit Scale with NEW window size - RECT rcNew; GetClientRect(hwnd, &rcNew); - float newFitScale = std::min((float)rcNew.right / imgSize.width, (float)rcNew.bottom / imgSize.height); - if (newFitScale > 0) g_viewState.Zoom = 1.0f / newFitScale; - } else { - // Standard logic (window size static) - RECT rc; GetClientRect(hwnd, &rc); - float fitScale = std::min((float)rc.right / imgSize.width, (float)rc.bottom / imgSize.height); - if (fitScale > 0) g_viewState.Zoom = 1.0f / fitScale; - } - - g_viewState.PanX = 0; - g_viewState.PanY = 0; - g_osd.Show(hwnd, L"Zoom: 100%", false, false, D2D1::ColorF(0.4f, 1.0f, 0.4f)); - } - } - InvalidateRect(hwnd, nullptr, FALSE); - break; - - case '0': case 'F': case VK_NUMPAD0: // Fit to Screen (Best Fit) - if (g_currentBitmap) { - // Get Monitor & Work Area - HMONITOR hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; GetMonitorInfoW(hMon, &mi); - int screenW = mi.rcWork.right - mi.rcWork.left; - int screenH = mi.rcWork.bottom - mi.rcWork.top; - - // Get Window Borders - RECT rcWin, rcClient; - GetWindowRect(hwnd, &rcWin); - GetClientRect(hwnd, &rcClient); - int borderW = (rcWin.right - rcWin.left) - (rcClient.right - rcClient.left); - int borderH = (rcWin.bottom - rcWin.top) - (rcClient.bottom - rcClient.top); - - // Max Client Size available - int maxClientW = screenW - borderW; - int maxClientH = screenH - borderH; - - // Get Image Size (DIPs -> Pixels) - D2D1_SIZE_F imgSize = g_currentBitmap->GetSize(); - float dpi = 96.0f; - UINT dpiX, dpiY; - if (SUCCEEDED(GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, &dpiX, &dpiY))) { - dpi = (float)dpiX; - } - float imgPixW = imgSize.width * (dpi / 96.0f); - float imgPixH = imgSize.height * (dpi / 96.0f); - - if (imgPixW > 0 && imgPixH > 0) { - // Scale to fit max client area - float ratioW = (float)maxClientW / imgPixW; - float ratioH = (float)maxClientH / imgPixH; - float scale = std::min(ratioW, ratioH); - - // Target Client Size - int targetClientW = (int)(imgPixW * scale); - int targetClientH = (int)(imgPixH * scale); - -<<<<<<< HEAD - int totalW = targetClientW + borderW; - int totalH = targetClientH + borderH; - - // Center - int cX = mi.rcWork.left + (screenW - totalW) / 2; - int cY = mi.rcWork.top + (screenH - totalH) / 2; - - SetWindowPos(hwnd, nullptr, cX, cY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE); - - // Set Zoom to match fit - RECT rcNew; GetClientRect(hwnd, &rcNew); - float fitScale = std::min((float)rcNew.right / imgSize.width, (float)rcNew.bottom / imgSize.height); - g_viewState.Zoom = 1.0f / fitScale; // This fits image exactly in window? - // Wait, Logic: FinalScale = FitScale * Zoom. If we want FinalScale = FitScale, then Zoom = 1.0? - // Line 3236: float finalScale = fitScale * g_viewState.Zoom; - // So if we want to "Fit to Screen" (image fills window), Zoom should be 1.0f. - g_viewState.Zoom = 1.0f; - } -======= - // Target Window Size - int targetWinW = targetClientW + borderW; - int targetWinH = targetClientH + borderH; - - // Center on Screen - int x = mi.rcWork.left + (screenW - targetWinW) / 2; - int y = mi.rcWork.top + (screenH - targetWinH) / 2; - - SetWindowPos(hwnd, nullptr, x, y, targetWinW, targetWinH, SWP_NOZORDER | SWP_NOACTIVATE); - } - - // Reset View to Fit - g_viewState.Zoom = 1.0f; ->>>>>>> 86ab245bf3828b79c29a67446c97cc74c452c2d1 - g_viewState.PanX = 0; - g_viewState.PanY = 0; - - g_osd.Show(hwnd, L"Zoom: Fit Screen", false, false, D2D1::ColorF(D2D1::ColorF::White)); - InvalidateRect(hwnd, nullptr, FALSE); - } -<<<<<<< HEAD - break; - - case VK_ADD: case VK_OEM_PLUS: // Zoom In - case VK_SUBTRACT: case VK_OEM_MINUS: { // Zoom Out - bool isZoomIn = (wParam == VK_ADD || wParam == VK_OEM_PLUS); - float step = ctrl ? 0.01f : 0.1f; // 1% or 10% - - if (isZoomIn) g_viewState.Zoom += step; - else g_viewState.Zoom -= step; - - if (g_viewState.Zoom < 0.01f) g_viewState.Zoom = 0.01f; - if (g_viewState.Zoom > 50.0f) g_viewState.Zoom = 50.0f; - - // Show OSD - wchar_t buf[32]; - swprintf_s(buf, L"Zoom: %.0f%%", g_viewState.Zoom * 100.0f); - g_osd.Show(hwnd, buf, false); - InvalidateRect(hwnd, nullptr, FALSE); -======= ->>>>>>> 86ab245bf3828b79c29a67446c97cc74c452c2d1 - break; - } - - // Fullscreen - case VK_RETURN: case VK_F11: // Enter/F11: Toggle fullscreen - SendMessage(hwnd, WM_COMMAND, IDM_FULLSCREEN, 0); - break; - - // Exit - case VK_ESCAPE: - if (IsZoomed(hwnd)) { - ShowWindow(hwnd, SW_RESTORE); - } else { - if (CheckUnsavedChanges(hwnd)) PostQuitMessage(0); - } - break; - } - return 0; - } - - case WM_RBUTTONUP: { - // Show context menu - POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; - ClientToScreen(hwnd, &pt); - - bool hasImage = g_currentBitmap != nullptr; - bool extensionFixNeeded = false; - bool isRaw = false; - if (hasImage && !g_imagePath.empty()) { - extensionFixNeeded = CheckExtensionMismatch(g_imagePath, g_currentMetadata.Format); - isRaw = IsRawFile(g_imagePath); - } - - ShowContextMenu(hwnd, pt, hasImage, extensionFixNeeded, g_config.LockWindowSize, g_config.ShowInfoPanel, g_config.AlwaysOnTop, g_config.RenderRAW, isRaw); - return 0; - } - - case WM_COMMAND: { - UINT cmdId = LOWORD(wParam); - switch (cmdId) { - case IDM_OPEN: { - if (!CheckUnsavedChanges(hwnd)) break; - OPENFILENAMEW ofn = {}; - wchar_t szFile[MAX_PATH] = {}; - ofn.lStructSize = sizeof(ofn); - ofn.hwndOwner = hwnd; - ofn.lpstrFile = szFile; - ofn.nMaxFile = MAX_PATH; - ofn.lpstrFilter = L"All Images\0*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.webp;*.avif;*.jxl;*.heic;*.heif;*.tga;*.psd;*.hdr;*.exr;*.svg;*.qoi;*.pcx;*.raw;*.arw;*.cr2;*.cr3;*.nef;*.dng;*.orf;*.rw2;*.raf;*.pef;*.pgm;*.ppm\0All Files\0*.*\0"; - ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; - if (GetOpenFileNameW(&ofn)) { - g_editState.Reset(); - g_viewState.Reset(); - g_navigator.Initialize(szFile); - g_thumbMgr.ClearCache(); // Fix: Clear old thumbnails on folder switch - LoadImageAsync(hwnd, szFile); - } - break; - } - case IDM_OPENWITH_DEFAULT: { - // Use rundll32 to show proper "Open With" dialog - if (!g_imagePath.empty()) { - std::wstring args = L"shell32.dll,OpenAs_RunDLL " + g_imagePath; - ShellExecuteW(hwnd, nullptr, L"rundll32.exe", args.c_str(), nullptr, SW_SHOWNORMAL); - } - break; - } - case IDM_EDIT: { - // Open with default editor (use "edit" verb, fallback to mspaint) - if (!g_imagePath.empty()) { - HINSTANCE result = ShellExecuteW(hwnd, L"edit", g_imagePath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); - if ((intptr_t)result <= 32) { - // No editor registered, try mspaint - ShellExecuteW(hwnd, nullptr, L"mspaint.exe", g_imagePath.c_str(), nullptr, SW_SHOWNORMAL); - } - } - break; - } - case IDM_SHOW_IN_EXPLORER: { - if (!g_imagePath.empty()) { - std::wstring cmd = L"/select,\"" + g_imagePath + L"\""; - ShellExecuteW(nullptr, nullptr, L"explorer.exe", cmd.c_str(), nullptr, SW_SHOWNORMAL); - } - break; - } - case IDM_COPY_PATH: { - if (!g_imagePath.empty() && OpenClipboard(hwnd)) { - EmptyClipboard(); - size_t len = (g_imagePath.length() + 1) * sizeof(wchar_t); - HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len); - if (hMem) { - memcpy(GlobalLock(hMem), g_imagePath.c_str(), len); - GlobalUnlock(hMem); - SetClipboardData(CF_UNICODETEXT, hMem); - } - CloseClipboard(); - g_osd.Show(hwnd, L"Path copied", false); - // Ensure UI updates to show OSD - InvalidateRect(hwnd, nullptr, FALSE); - } - break; - } - case IDM_COPY_IMAGE: { - // Copy file to clipboard (can paste in Explorer or other apps) - if (!g_imagePath.empty() && OpenClipboard(hwnd)) { - EmptyClipboard(); - - // CF_HDROP format for file copy - size_t pathLen = (g_imagePath.length() + 1) * sizeof(wchar_t); - size_t totalSize = sizeof(DROPFILES) + pathLen + sizeof(wchar_t); // Extra null for double-null terminator - HGLOBAL hDrop = GlobalAlloc(GHND, totalSize); - if (hDrop) { - DROPFILES* df = (DROPFILES*)GlobalLock(hDrop); - df->pFiles = sizeof(DROPFILES); - df->fWide = TRUE; - memcpy((char*)df + sizeof(DROPFILES), g_imagePath.c_str(), pathLen); - GlobalUnlock(hDrop); - SetClipboardData(CF_HDROP, hDrop); - } - - CloseClipboard(); - g_osd.Show(hwnd, L"File copied to clipboard", false); - InvalidateRect(hwnd, nullptr, FALSE); - } - break; - } - case IDM_PRINT: { - if (!g_imagePath.empty()) { - // Windows 10/11: Use "print" verb directly - Windows handles the print dialog - // This works for most image formats via Windows photo printing - HINSTANCE result = ShellExecuteW(hwnd, L"print", g_imagePath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); - - if ((intptr_t)result <= 32) { - // Fallback: Open in default app and show OSD instructions - ShellExecuteW(hwnd, L"open", g_imagePath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); - g_osd.Show(hwnd, L"Print: Use Ctrl+P in opened app", false); - InvalidateRect(hwnd, nullptr, FALSE); - } - } - break; - } - case IDM_FULLSCREEN: { - if (IsZoomed(hwnd)) ShowWindow(hwnd, SW_RESTORE); - else ShowWindow(hwnd, SW_MAXIMIZE); - break; - } - case IDM_DELETE: { - if (!g_imagePath.empty()) { - // Get filename for display - size_t lastSlash = g_imagePath.find_last_of(L"\\/"); - std::wstring filename = (lastSlash != std::wstring::npos) ? g_imagePath.substr(lastSlash + 1) : g_imagePath; - - // OSD-style confirmation dialog (consistent with rotation save dialog) - std::wstring dlgMessage = L"Move to Recycle Bin?"; - std::vector dlgButtons; - dlgButtons.emplace_back(DialogResult::Yes, L"Delete"); - dlgButtons.emplace_back(DialogResult::Cancel, L"Cancel"); - - // Red accent color for delete warning - DialogResult dlgResult = ShowQuickViewDialog(hwnd, filename.c_str(), dlgMessage.c_str(), - D2D1::ColorF(0.85f, 0.25f, 0.25f), dlgButtons, false, L"", L""); - - if (dlgResult == DialogResult::Yes) { - std::wstring nextPath = g_navigator.PeekNext(); - if (nextPath == g_imagePath) nextPath = g_navigator.PeekPrevious(); - - // Release image before delete - ReleaseImageResources(); - - // Use SHFileOperation for recycle bin - std::wstring pathCopy = g_imagePath; - pathCopy.push_back(L'\0'); // Double null terminator - SHFILEOPSTRUCTW op = {}; - op.wFunc = FO_DELETE; - op.pFrom = pathCopy.c_str(); - op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT; - if (SHFileOperationW(&op) == 0) { - g_osd.Show(hwnd, L"Moved to Recycle Bin", false); - InvalidateRect(hwnd, nullptr, FALSE); - g_editState.Reset(); - g_viewState.Reset(); - g_currentBitmap.Reset(); - g_navigator.Initialize(nextPath.empty() ? L"" : nextPath.c_str()); - if (!nextPath.empty()) LoadImageAsync(hwnd, nextPath); - else InvalidateRect(hwnd, nullptr, FALSE); - } - } - } - break; - } - case IDM_LOCK_WINDOW_SIZE: { - g_config.LockWindowSize = !g_config.LockWindowSize; - g_toolbar.SetLockState(g_config.LockWindowSize); - g_osd.Show(hwnd, g_config.LockWindowSize ? L"Window Size Locked" : L"Window Size Unlocked", false); - InvalidateRect(hwnd, nullptr, FALSE); - break; - } - case IDM_SHOW_INFO_PANEL: { - g_config.ShowInfoPanel = !g_config.ShowInfoPanel; - if (g_config.ShowInfoPanel && g_currentMetadata.HistR.empty() && !g_imagePath.empty()) { - UpdateHistogramAsync(hwnd, g_imagePath); - } - - // Refresh Title (Just simple name now, Overlay handles info) - { - std::wstring title = L"QuickView"; - if (!g_imagePath.empty()) { - size_t lastSlash = g_imagePath.find_last_of(L"\\/"); - std::wstring rname = (lastSlash != std::wstring::npos) ? g_imagePath.substr(lastSlash + 1) : g_imagePath; - title = rname + L" - " + title; - } - } - - g_toolbar.SetExifState(g_config.ShowInfoPanel); - InvalidateRect(hwnd, nullptr, FALSE); - break; - } - case IDM_ALWAYS_ON_TOP: { - g_config.AlwaysOnTop = !g_config.AlwaysOnTop; - SetWindowPos(hwnd, g_config.AlwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, - 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - g_osd.Show(hwnd, g_config.AlwaysOnTop ? L"Always on Top: ON" : L"Always on Top: OFF", false); - InvalidateRect(hwnd, nullptr, FALSE); - break; - } - - case IDM_HUD_GALLERY: - // Toggle Gallery - SendMessage(hwnd, WM_KEYDOWN, 'T', 0); - break; - - case IDM_LITE_INFO: - SendMessage(hwnd, WM_KEYDOWN, VK_TAB, 0); - break; - - case IDM_ZOOM_100: - SendMessage(hwnd, WM_KEYDOWN, '1', 0); - break; - - case IDM_ZOOM_FIT: - SendMessage(hwnd, WM_KEYDOWN, '0', 0); - break; - - case IDM_ZOOM_IN: - // Simulate Key Press - SendMessage(hwnd, WM_KEYDOWN, VK_ADD, 0); - break; - case IDM_ZOOM_OUT: - SendMessage(hwnd, WM_KEYDOWN, VK_SUBTRACT, 0); - break; - - case IDM_ROTATE_CW: - PerformTransform(hwnd, TransformType::Rotate90CW); - break; - case IDM_ROTATE_CCW: - PerformTransform(hwnd, TransformType::Rotate90CCW); - break; - case IDM_FLIP_H: - PerformTransform(hwnd, TransformType::FlipHorizontal); - break; - case IDM_FLIP_V: - PerformTransform(hwnd, TransformType::FlipVertical); - break; - - case IDM_RENDER_RAW: - g_config.RenderRAW = !g_config.RenderRAW; - g_toolbar.SetRawState(g_config.RenderRAW, false); - if (!g_imagePath.empty()) { - LoadImageAsync(hwnd, g_imagePath.c_str()); - } - InvalidateRect(hwnd, nullptr, FALSE); - break; - - case IDM_WALLPAPER_FILL: - case IDM_WALLPAPER_FIT: - case IDM_WALLPAPER_TILE: { - if (!g_imagePath.empty()) { - // Use IDesktopWallpaper COM interface - CoInitialize(nullptr); - IDesktopWallpaper* pWallpaper = nullptr; - HRESULT hr = CoCreateInstance(__uuidof(DesktopWallpaper), nullptr, CLSCTX_ALL, - IID_PPV_ARGS(&pWallpaper)); - if (SUCCEEDED(hr) && pWallpaper) { - DESKTOP_WALLPAPER_POSITION pos = DWPOS_FILL; - if (cmdId == IDM_WALLPAPER_FIT) pos = DWPOS_FIT; - else if (cmdId == IDM_WALLPAPER_TILE) pos = DWPOS_TILE; - - pWallpaper->SetPosition(pos); - hr = pWallpaper->SetWallpaper(nullptr, g_imagePath.c_str()); - pWallpaper->Release(); - - if (SUCCEEDED(hr)) { - g_osd.Show(hwnd, L"Wallpaper Set", false); - } else { - g_osd.Show(hwnd, L"Failed to set wallpaper", true); - } - InvalidateRect(hwnd, nullptr, FALSE); - } - CoUninitialize(); - } - break; - } - case IDM_RENAME: { - if (!g_imagePath.empty()) { - // Get current filename - size_t lastSlash = g_imagePath.find_last_of(L"\\/"); - std::wstring dir = (lastSlash != std::wstring::npos) ? g_imagePath.substr(0, lastSlash + 1) : L""; - std::wstring oldName = (lastSlash != std::wstring::npos) ? g_imagePath.substr(lastSlash + 1) : g_imagePath; - - // Custom Rename Dialog - std::wstring newName = ShowRenameDialog(hwnd, oldName); - - if (!newName.empty()) { - // Auto-append extension if missing (User request) - if (newName.find_last_of(L'.') == std::wstring::npos) { - size_t oldDot = oldName.find_last_of(L'.'); - if (oldDot != std::wstring::npos) { - newName += oldName.substr(oldDot); - } - } - - if (newName != oldName) { - std::wstring newPath = dir + newName; - if (newPath != g_imagePath) { - ReleaseImageResources(); - if (MoveFileW(g_imagePath.c_str(), newPath.c_str())) { - g_imagePath = newPath; - g_navigator.Initialize(newPath.c_str()); - LoadImageAsync(hwnd, newPath); - g_osd.Show(hwnd, L"Renamed", false); - } else { - LoadImageAsync(hwnd, g_imagePath); - g_osd.Show(hwnd, L"Rename Failed", true); - } - InvalidateRect(hwnd, nullptr, FALSE); - } - } - } - } - break; - } - case IDM_FIX_EXTENSION: { - if (!g_imagePath.empty() && !g_currentMetadata.Format.empty()) { - std::wstring fmt = g_currentMetadata.Format; - std::transform(fmt.begin(), fmt.end(), fmt.begin(), ::towlower); - - std::wstring newExt; - if (fmt == L"jpeg") newExt = L".jpg"; - else if (fmt == L"png") newExt = L".png"; - else if (fmt == L"webp") newExt = L".webp"; - else if (fmt == L"avif") newExt = L".avif"; - else if (fmt == L"jxl" || fmt == L"jpeg xl") newExt = L".jxl"; - else if (fmt == L"gif") newExt = L".gif"; - else if (fmt == L"bmp") newExt = L".bmp"; - else if (fmt == L"tiff") newExt = L".tiff"; - else if (fmt == L"heif" || fmt == L"heic") newExt = L".heic"; - else if (fmt == L"hdr") newExt = L".hdr"; - else if (fmt == L"psd") newExt = L".psd"; - else if (fmt == L"tga") newExt = L".tga"; - else if (fmt == L"exr") newExt = L".exr"; - else if (fmt == L"qoi") newExt = L".qoi"; - else if (fmt == L"pcx") newExt = L".pcx"; - else if (fmt == L"svg") newExt = L".svg"; - else if (fmt == L"ico") newExt = L".ico"; - else if (fmt == L"wbmp") newExt = L".wbmp"; - else if (fmt == L"pic") newExt = L".pic"; - else if (fmt == L"pnm") newExt = L".pnm"; - - if (!newExt.empty()) { - size_t lastDot = g_imagePath.find_last_of(L'.'); - std::wstring basePath = (lastDot != std::wstring::npos) ? g_imagePath.substr(0, lastDot) : g_imagePath; - std::wstring newPath = basePath + newExt; - - std::wstring msg = L"Format detected: " + g_currentMetadata.Format + L"\nChange extension to " + newExt + L"?"; - - std::vector buttons = { - { DialogResult::Yes, L"Rename", true }, - { DialogResult::Cancel, L"Cancel" } - }; - - DialogResult result = ShowQuickViewDialog(hwnd, L"Fix Extension", msg, D2D1::ColorF(D2D1::ColorF::Orange), buttons); - if (result == DialogResult::Yes) { - ReleaseImageResources(); - if (MoveFileW(g_imagePath.c_str(), newPath.c_str())) { - g_imagePath = newPath; - LoadImageAsync(hwnd, newPath); - g_osd.Show(hwnd, L"Extension Fixed", false); - } else { - LoadImageAsync(hwnd, g_imagePath); // Reload old - g_osd.Show(hwnd, L"Rename Failed", true); - } - } - InvalidateRect(hwnd, nullptr, FALSE); - } - } - break; - } - case IDM_SETTINGS: { - // Simple Settings / Help Dialog - std::wstring msg = L"Shortcuts:\n" - L"Double Click: Full Screen\n" - L"Wheel: Zoom\n" - L"Drag: Pan\n" - L"Hold Shift + R: Rotate Left\n" - L"R, H, V: Rotate/Flip\n" - L"Arrows/Space: Navigate\n" - L"Esc/MButton: Exit"; - - std::vector buttons = { - { DialogResult::Yes, L"OK", true } - }; - ShowQuickViewDialog(hwnd, L"QuickView Settings", msg, D2D1::ColorF(D2D1::ColorF::DodgerBlue), buttons); - InvalidateRect(hwnd, nullptr, FALSE); - break; - } - case IDM_ABOUT: { - MessageBoxW(hwnd, L"QuickView\n\nHigh Performance Image Viewer\n\n(c) 2024-2026", L"About QuickView", MB_ICONINFORMATION); - break; - } - case IDM_EXIT: { - if (CheckUnsavedChanges(hwnd)) PostMessage(hwnd, WM_CLOSE, 0, 0); - break; - } - // TODO: Implement other menu commands - default: - break; - } - return 0; - } - } - return DefWindowProc(hwnd, message, wParam, lParam); -} - -void OnResize(HWND hwnd, UINT width, UINT height) { - if (g_renderEngine) g_renderEngine->Resize(width, height); - g_toolbar.UpdateLayout((float)width, (float)height); -} -FireAndForget PrefetchImageAsync(HWND hwnd, std::wstring path); // fwd decl - - -FireAndForget UpdateHistogramAsync(HWND hwnd, std::wstring path) { - if (path.empty() || path != g_imagePath) co_return; - - co_await ResumeBackground{}; - - ComPtr tempBitmap; - std::wstring loaderName; // dummy - if (SUCCEEDED(g_imageLoader->LoadToMemory(path.c_str(), &tempBitmap, &loaderName))) { - CImageLoader::ImageMetadata histMeta; - g_imageLoader->ComputeHistogram(tempBitmap.Get(), &histMeta); - - co_await ResumeMainThread(hwnd); - - if (path == g_imagePath) { - g_currentMetadata.HistR = histMeta.HistR; - g_currentMetadata.HistG = histMeta.HistG; - g_currentMetadata.HistB = histMeta.HistB; - g_currentMetadata.HistL = histMeta.HistL; - InvalidateRect(hwnd, nullptr, FALSE); - } - } -} - -FireAndForget LoadImageAsync(HWND hwnd, std::wstring path) { - if (!g_imageLoader || !g_renderEngine) co_return; - - // 1. Check Prefetch Cache (Main Thread) - ComPtr wicMemoryBitmap; - bool usedPrefetchCache = false; - if (path == g_prefetchedPath && g_prefetchedBitmap) { - wicMemoryBitmap = g_prefetchedBitmap; - usedPrefetchCache = true; - // Note: FormatDetails from prefetch may be stale, will be cleared later - } - - // 2. If Miss, Load from Disk - if (!wicMemoryBitmap) { - // Switch to Background Thread - co_await ResumeBackground{}; - - // Decode & Measure Time - DWORD startTime = GetTickCount(); - std::wstring loaderName = L"Unknown"; - HRESULT hr = g_imageLoader->LoadToMemory(path.c_str(), &wicMemoryBitmap, &loaderName, g_config.ForceRawDecode); - - // Read Metadata (Background Thread) - // Read Metadata (Background Thread) - CImageLoader::ImageMetadata tempMetadata; - if (SUCCEEDED(hr)) { - g_imageLoader->ReadMetadata(path.c_str(), &tempMetadata); - - // Compute Histogram if needed (Expanded Mode only) - if (g_config.ShowInfoPanel && g_config.InfoPanelExpanded && wicMemoryBitmap) { - g_imageLoader->ComputeHistogram(wicMemoryBitmap.Get(), &tempMetadata); - } - } - - DWORD duration = GetTickCount() - startTime; - - // Switch back - co_await ResumeMainThread(hwnd); - - if (FAILED(hr) || !wicMemoryBitmap) { - // Error Handling - - // Check for HEIC Missing Codec - bool isHEIC = path.ends_with(L".heic") || path.ends_with(L".HEIC") || - path.ends_with(L".heif") || path.ends_with(L".HEIF"); - - if (isHEIC && (hr == WINCODEC_ERR_COMPONENTNOTFOUND || hr == E_FAIL)) { - // User needs HEVC extension - g_osd.Show(hwnd, AppStrings::OSD_HEICCodecMissing, true); - } else { - g_osd.Show(hwnd, L"Failed to load image", true); - } - co_return; - } - - g_currentMetadata = tempMetadata; - // FormatDetails: only valid if freshly loaded (not from prefetch cache) - if (usedPrefetchCache) { - g_currentMetadata.FormatDetails.clear(); // Prefetch doesn't preserve format details - g_currentMetadata.LoaderName = L"Prefetch Cache"; - g_currentMetadata.LoadTimeMs = 0; - } else { - g_currentMetadata.FormatDetails = g_imageLoader->GetLastFormatDetails(); - g_currentMetadata.LoaderName = loaderName; - g_currentMetadata.LoadTimeMs = duration; - } - - // Update Title with Performance Info AND Compact Metadata - // Format: "filename.ext - [Metadata Compact] - QuickView" - size_t lastSlash = path.find_last_of(L"\\/"); - std::wstring filename = (lastSlash != std::wstring::npos) ? path.substr(lastSlash + 1) : path; - - wchar_t titleBuf[1024]; - swprintf_s(titleBuf, L"%s - %s (%lu ms) - %s", filename.c_str(), loaderName.c_str(), duration, g_szWindowTitle); - SetWindowTextW(hwnd, titleBuf); - // Decoder info now shown in Info Panel instead of OSD - } - else { - // Cache Hit - still need to load metadata - CImageLoader::ImageMetadata tempMetadata; - g_imageLoader->ReadMetadata(path.c_str(), &tempMetadata); - - // Compute Histogram if needed (Offload to background to avoid jank) - // Optimization: Only if Expanded - if (g_config.ShowInfoPanel && g_config.InfoPanelExpanded && wicMemoryBitmap) { - co_await ResumeBackground{}; - g_imageLoader->ComputeHistogram(wicMemoryBitmap.Get(), &tempMetadata); - co_await ResumeMainThread(hwnd); - } - g_currentMetadata = tempMetadata; - g_currentMetadata.LoaderName = L"Prefetch Cache"; - g_currentMetadata.LoadTimeMs = 0; - g_currentMetadata.FormatDetails.clear(); // Prefetch doesn't preserve format details - - size_t lastSlash = path.find_last_of(L"\\/"); - std::wstring filename = (lastSlash != std::wstring::npos) ? path.substr(lastSlash + 1) : path; - wchar_t titleBuf[512]; - swprintf_s(titleBuf, L"%s - Prefetch Cache (0 ms) - %s", filename.c_str(), g_szWindowTitle); - SetWindowTextW(hwnd, titleBuf); - // Decoder info now shown in Info Panel instead of OSD - } - - // 3. Upload to GPU - ComPtr d2dBitmap; - if (FAILED(g_renderEngine->CreateBitmapFromWIC(wicMemoryBitmap.Get(), &d2dBitmap))) { - g_osd.Show(hwnd, L"Failed to upload texture", true); - co_return; - } - - // 4. Update State - g_currentBitmap = d2dBitmap; - g_imagePath = path; - - // Update Toolbar RAW State - bool isRaw = false; - std::wstring pLower = path; - std::transform(pLower.begin(), pLower.end(), pLower.begin(), ::towlower); - if (pLower.ends_with(L".arw") || pLower.ends_with(L".cr2") || pLower.ends_with(L".nef") || - pLower.ends_with(L".dng") || pLower.ends_with(L".orf") || pLower.ends_with(L".rw2") || - pLower.ends_with(L".raf") || pLower.ends_with(L".pef") || pLower.ends_with(L".srw") || pLower.ends_with(L".cr3")) { - isRaw = true; - } - g_toolbar.SetRawState(isRaw, g_config.ForceRawDecode); - - // Update Toolbar Extension Warning - bool needsFix = false; - if (!g_currentMetadata.Format.empty()) { - needsFix = CheckExtensionMismatch(path, g_currentMetadata.Format); - } - g_toolbar.SetExtensionWarning(needsFix); - - // 5. Adjust & Repaint - AdjustWindowToImage(hwnd); - InvalidateRect(hwnd, nullptr, FALSE); - - // 6. Trigger Prefetch for NEXT image - if (g_navigator.Count() > 0) { - std::wstring nextPath = g_navigator.PeekNext(); - if (!nextPath.empty() && nextPath != g_prefetchedPath && nextPath != path) { - PrefetchImageAsync(hwnd, nextPath); - } - } -} - -FireAndForget PrefetchImageAsync(HWND hwnd, std::wstring path) { - if (path.empty() || path == g_prefetchedPath) co_return; - - co_await ResumeBackground{}; - - ComPtr bitmap; - HRESULT hr = g_imageLoader->LoadToMemory(path.c_str(), &bitmap); - - co_await ResumeMainThread(hwnd); - - if (SUCCEEDED(hr) && bitmap) { - g_prefetchedBitmap = bitmap; - g_prefetchedPath = path; - // g_osd.Show(L"Prefetched", false, false); // Debug - } -} - -void Navigate(HWND hwnd, int direction) { - if (g_navigator.Count() <= 0) return; - if (CheckUnsavedChanges(hwnd)) { - std::wstring path = (direction > 0) ? g_navigator.Next() : g_navigator.Previous(); - if (!path.empty()) { - g_editState.Reset(); - g_viewState.Reset(); - LoadImageAsync(hwnd, path); - } - } -} - -void DrawHistogram(ID2D1DeviceContext* context, const CImageLoader::ImageMetadata& meta, D2D1_RECT_F rect) { - if (meta.HistR.empty()) return; // Need RGB data - - // Find max across all channels for consistent scaling - uint32_t maxVal = 1; - for (int i = 0; i < 256; i++) { - if (meta.HistR[i] > maxVal) maxVal = meta.HistR[i]; - if (meta.HistG[i] > maxVal) maxVal = meta.HistG[i]; - if (meta.HistB[i] > maxVal) maxVal = meta.HistB[i]; - } - - float stepX = (rect.right - rect.left) / 256.0f; - float bottom = rect.bottom; - float height = rect.bottom - rect.top; - - // Helper lambda to draw a single channel - auto drawChannel = [&](const std::vector& hist, D2D1::ColorF color) { - ComPtr path; - g_renderEngine->m_d2dFactory->CreatePathGeometry(&path); - ComPtr sink; - path->Open(&sink); - - sink->BeginFigure(D2D1::Point2F(rect.left, bottom), D2D1_FIGURE_BEGIN_FILLED); - for (int i = 0; i < 256; i++) { - float val = (float)hist[i] / maxVal; - float y = bottom - val * height; - sink->AddLine(D2D1::Point2F(rect.left + i * stepX, y)); - } - sink->AddLine(D2D1::Point2F(rect.right, bottom)); - sink->EndFigure(D2D1_FIGURE_END_CLOSED); - sink->Close(); - - ComPtr brush; - context->CreateSolidColorBrush(color, &brush); - context->FillGeometry(path.Get(), brush.Get()); - }; - - // Draw RGB with semi-transparency (order: B, G, R for nice overlap) - drawChannel(meta.HistB, D2D1::ColorF(0.0f, 0.4f, 1.0f, 0.4f)); // Blue - drawChannel(meta.HistG, D2D1::ColorF(0.2f, 0.9f, 0.3f, 0.4f)); // Green - drawChannel(meta.HistR, D2D1::ColorF(1.0f, 0.3f, 0.3f, 0.4f)); // Red -} - -void DrawCompactInfo(ID2D1DeviceContext* context) { - if (g_imagePath.empty()) return; - - if (!g_pPanelTextFormat) { - g_renderEngine->m_dwriteFactory->CreateTextFormat( - L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, - 13.0f, L"en-us", &g_pPanelTextFormat - ); - // Set no-wrap mode to prevent long filenames from wrapping - if (g_pPanelTextFormat) { - g_pPanelTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); - } - } - - std::wstring info = g_imagePath.substr(g_imagePath.find_last_of(L"\\/") + 1); - - // Add Size - if (g_currentMetadata.Width > 0) { - wchar_t sz[64]; swprintf_s(sz, L" %u x %u", g_currentMetadata.Width, g_currentMetadata.Height); - info += sz; - - // File Size - if (g_currentMetadata.FileSize > 0) { - double mb = g_currentMetadata.FileSize / (1024.0 * 1024.0); - swprintf_s(sz, L" %.2f MB", mb); - info += sz; - } - } - // Add Compact EXIF - std::wstring meta = g_currentMetadata.GetCompactString(); - if (!meta.empty()) info += L" " + meta; - - // Measure Text - float textW = MeasureTextWidth(info, g_pPanelTextFormat.Get()); - float totalW = textW + 70.0f; // Padding for 2 buttons - - D2D1_RECT_F rect = D2D1::RectF(20, 10, 20 + textW, 40); - D2D1_RECT_F bgRect = D2D1::RectF(20, 10, 20 + totalW + 10, 40); - - // Shadow Text - ComPtr brushShadow; - context->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.8f), &brushShadow); - D2D1_RECT_F shadowRect = D2D1::RectF(rect.left + 1, rect.top + 1, rect.right + 1, rect.bottom + 1); - context->DrawTextW(info.c_str(), (UINT32)info.length(), g_pPanelTextFormat.Get(), shadowRect, brushShadow.Get()); - - // Text - ComPtr brushText; - context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &brushText); - context->DrawTextW(info.c_str(), (UINT32)info.length(), g_pPanelTextFormat.Get(), rect, brushText.Get()); - - // Expand Button [+] - Yellow with shadow - g_panelToggleRect = D2D1::RectF(rect.right + 8, rect.top, rect.right + 38, rect.bottom); - - // Shadow - D2D1_RECT_F shadowRect1 = D2D1::RectF(g_panelToggleRect.left + 1, g_panelToggleRect.top + 1, g_panelToggleRect.right + 1, g_panelToggleRect.bottom + 1); - context->DrawTextW(L"[+]", 3, g_pPanelTextFormat.Get(), shadowRect1, brushShadow.Get()); - - ComPtr brushYellow; - context->CreateSolidColorBrush(D2D1::ColorF(1.0f, 0.85f, 0.0f), &brushYellow); // Warm yellow - context->DrawTextW(L"[+]", 3, g_pPanelTextFormat.Get(), g_panelToggleRect, brushYellow.Get()); - - // Close Button [x] - Red with shadow - g_panelCloseRect = D2D1::RectF(g_panelToggleRect.right + 5, rect.top, g_panelToggleRect.right + 35, rect.bottom); - - // Shadow - D2D1_RECT_F shadowRect2 = D2D1::RectF(g_panelCloseRect.left + 1, g_panelCloseRect.top + 1, g_panelCloseRect.right + 1, g_panelCloseRect.bottom + 1); - context->DrawTextW(L"[x]", 3, g_pPanelTextFormat.Get(), shadowRect2, brushShadow.Get()); - - ComPtr brushRed; - context->CreateSolidColorBrush(D2D1::ColorF(1.0f, 0.3f, 0.3f), &brushRed); // Soft red - context->DrawTextW(L"[x]", 3, g_pPanelTextFormat.Get(), g_panelCloseRect, brushRed.Get()); -} - -// ============================================================================ -// Grid-Based Info Panel System -// ============================================================================ - -// Build info grid data from current metadata -void BuildInfoGrid() { - g_infoGrid.clear(); - - // Row 1: Filename - std::wstring filename = g_imagePath.substr(g_imagePath.find_last_of(L"\\/") + 1); - g_infoGrid.push_back({L"\U0001F4C4", L"File", filename, L"", filename, TruncateMode::MiddleEllipsis, true}); - - // Row 2: Dimensions + Megapixels - if (g_currentMetadata.Width > 0) { - UINT64 totalPixels = (UINT64)g_currentMetadata.Width * g_currentMetadata.Height; - double megapixels = totalPixels / 1000000.0; - wchar_t dimBuf[64]; - // Use 'x' instead of multiplication sign to avoid mojibake - swprintf_s(dimBuf, L"%u x %u", g_currentMetadata.Width, g_currentMetadata.Height); - wchar_t mpBuf[32]; - swprintf_s(mpBuf, L"(%.1f MP)", megapixels); - g_infoGrid.push_back({L"\U0001F4D0", L"Size", dimBuf, mpBuf, L"", TruncateMode::None, false}); - } - - // Row 3: File Size - if (g_currentMetadata.FileSize > 0) { - UINT64 bytes = g_currentMetadata.FileSize; - wchar_t sizeBuf[32]; - if (bytes >= 1024 * 1024) { - swprintf_s(sizeBuf, L"%.2f MB", bytes / (1024.0 * 1024.0)); - } else if (bytes >= 1024) { - swprintf_s(sizeBuf, L"%.2f KB", bytes / 1024.0); - } else { - swprintf_s(sizeBuf, L"%llu B", bytes); - } - std::wstring sub = L"(" + FormatBytesWithCommas(bytes) + L")"; - std::wstring extra = g_currentMetadata.FormatDetails.empty() ? L"" : L" [" + g_currentMetadata.FormatDetails + L"]"; - g_infoGrid.push_back({L"\U0001F4BE", L"Disk", std::wstring(sizeBuf) + extra, sub, L"", TruncateMode::None, false}); - } - - // Row 4: Date - if (!g_currentMetadata.Date.empty()) { - g_infoGrid.push_back({L"\U0001F4C5", L"Date", g_currentMetadata.Date, L"", L"", TruncateMode::EndEllipsis, false}); - } - - // Row 5: Camera - if (!g_currentMetadata.Make.empty()) { - std::wstring camera = g_currentMetadata.Make + L" " + g_currentMetadata.Model; - g_infoGrid.push_back({L"\U0001F4F7", L"Camera", camera, L"", camera, TruncateMode::EndEllipsis, false}); - } - - // Row 6: Exposure (ISO + Aperture + Shutter + Bias) - if (!g_currentMetadata.ISO.empty()) { - std::wstring exp = L"ISO " + g_currentMetadata.ISO + L" " + g_currentMetadata.Aperture + L" " + g_currentMetadata.Shutter; - std::wstring sub = g_currentMetadata.ExposureBias.empty() ? L"" : g_currentMetadata.ExposureBias; - g_infoGrid.push_back({L"\U000026A1", L"Exp", exp, sub, exp + L" " + sub, TruncateMode::EndEllipsis, false}); - } - - // Row 7: Lens - if (!g_currentMetadata.Lens.empty()) { - g_infoGrid.push_back({L"\U0001F52D", L"Lens", g_currentMetadata.Lens, L"", g_currentMetadata.Lens, TruncateMode::EndEllipsis, false}); - } - - // Row 8: Focal - if (!g_currentMetadata.Focal.empty()) { - g_infoGrid.push_back({L"\U0001F3AF", L"Focal", g_currentMetadata.Focal, L"", L"", TruncateMode::None, false}); - } - - // Row 9: Color Space - if (!g_currentMetadata.ColorSpace.empty()) { - g_infoGrid.push_back({L"\U0001F3A8", L"Color", g_currentMetadata.ColorSpace, L"", L"", TruncateMode::None, false}); - } - - // Row 10: Decoder - if (!g_currentMetadata.LoaderName.empty()) { - wchar_t timeBuf[32]; - swprintf_s(timeBuf, L"(%llu ms)", (UINT64)g_currentMetadata.LoadTimeMs); - g_infoGrid.push_back({L"\U00002699", L"Decode", g_currentMetadata.LoaderName, timeBuf, L"", TruncateMode::None, false}); - } - - // Row 11: Software - if (!g_currentMetadata.Software.empty()) { - g_infoGrid.push_back({L"\U0001F4BB", L"Soft", g_currentMetadata.Software, L"", g_currentMetadata.Software, TruncateMode::EndEllipsis, false}); - } -} - -// Layout and draw the info grid -void DrawInfoGrid(ID2D1DeviceContext* context, float startX, float startY, float width) { - if (g_infoGrid.empty()) return; - - ComPtr brushWhite, brushGray, brushHover; - context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &brushWhite); - context->CreateSolidColorBrush(D2D1::ColorF(0.6f, 0.6f, 0.65f), &brushGray); - context->CreateSolidColorBrush(D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.1f), &brushHover); - - float valueColStart = startX + GRID_ICON_WIDTH + GRID_LABEL_WIDTH; - float valueColWidth = width - GRID_ICON_WIDTH - GRID_LABEL_WIDTH - GRID_PADDING; - float y = startY; - - for (size_t i = 0; i < g_infoGrid.size(); i++) { - auto& row = g_infoGrid[i]; - - // Calculate hit rect (for tooltip and click) - row.hitRect = D2D1::RectF(startX, y, startX + width, y + GRID_ROW_HEIGHT); - - // Hover highlight - if ((int)i == g_hoverRowIndex) { - context->FillRectangle(row.hitRect, brushHover.Get()); - } - - // Icon column - D2D1_RECT_F iconRect = D2D1::RectF(startX, y, startX + GRID_ICON_WIDTH, y + GRID_ROW_HEIGHT); - context->DrawTextW(row.icon.c_str(), (UINT32)row.icon.length(), g_pPanelTextFormat.Get(), iconRect, brushWhite.Get()); - - // Label column (gray) - D2D1_RECT_F labelRect = D2D1::RectF(startX + GRID_ICON_WIDTH, y, valueColStart, y + GRID_ROW_HEIGHT); - context->DrawTextW(row.label.c_str(), (UINT32)row.label.length(), g_pPanelTextFormat.Get(), labelRect, brushGray.Get()); - - // Value column - apply truncation - float subWidth = row.valueSub.empty() ? 0 : MeasureTextWidth(row.valueSub, g_pPanelTextFormat.Get()) + 5; - float mainMaxWidth = valueColWidth - subWidth; - - if (row.mode == TruncateMode::MiddleEllipsis) { - row.displayText = MakeMiddleEllipsis(g_pPanelTextFormat.Get(), mainMaxWidth, row.valueMain); - } else if (row.mode == TruncateMode::EndEllipsis) { - row.displayText = MakeEndEllipsis(g_pPanelTextFormat.Get(), mainMaxWidth, row.valueMain); - } else { - row.displayText = row.valueMain; - } - row.isTruncated = (row.displayText != row.valueMain); - - // Draw main value - D2D1_RECT_F valueRect = D2D1::RectF(valueColStart, y, valueColStart + mainMaxWidth, y + GRID_ROW_HEIGHT); - context->DrawTextW(row.displayText.c_str(), (UINT32)row.displayText.length(), g_pPanelTextFormat.Get(), valueRect, brushWhite.Get()); - - // Draw sub value (gray, smaller feel) - if (!row.valueSub.empty()) { - D2D1_RECT_F subRect = D2D1::RectF(valueColStart + mainMaxWidth, y, startX + width, y + GRID_ROW_HEIGHT); - context->DrawTextW(row.valueSub.c_str(), (UINT32)row.valueSub.length(), g_pPanelTextFormat.Get(), subRect, brushGray.Get()); - } - - y += GRID_ROW_HEIGHT; - } -} - -// Draw tooltip for hovered row -void DrawGridTooltip(ID2D1DeviceContext* context) { - if (g_hoverRowIndex < 0 || g_hoverRowIndex >= (int)g_infoGrid.size()) return; - - const auto& row = g_infoGrid[g_hoverRowIndex]; - if (!row.isTruncated || row.fullText.empty()) return; - - float x = (float)g_lastMousePos.x + 10; - float y = (float)g_lastMousePos.y + 20; - - float textWidth = MeasureTextWidth(row.fullText, g_pPanelTextFormat.Get()); - float padding = 6.0f; - float boxWidth = textWidth + padding * 2; - float boxHeight = 20.0f; - - D2D1_SIZE_F size = context->GetSize(); - if (x + boxWidth > size.width - 10) x = size.width - boxWidth - 10; - if (y + boxHeight > size.height - 10) y = size.height - boxHeight - 10; - - D2D1_RECT_F boxRect = D2D1::RectF(x, y, x + boxWidth, y + boxHeight); - - ComPtr brushBg, brushBorder, brushText; - context->CreateSolidColorBrush(D2D1::ColorF(0.1f, 0.1f, 0.12f, 0.95f), &brushBg); - context->CreateSolidColorBrush(D2D1::ColorF(0.4f, 0.4f, 0.45f), &brushBorder); - context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &brushText); - - context->FillRoundedRectangle(D2D1::RoundedRect(boxRect, 4.0f, 4.0f), brushBg.Get()); - context->DrawRoundedRectangle(D2D1::RoundedRect(boxRect, 4.0f, 4.0f), brushBorder.Get(), 1.0f); - - D2D1_RECT_F textRect = D2D1::RectF(x + padding, y + 2, x + boxWidth - padding, y + boxHeight); - context->DrawTextW(row.fullText.c_str(), (UINT32)row.fullText.length(), g_pPanelTextFormat.Get(), textRect, brushText.Get()); -} - -void DrawInfoPanel(ID2D1DeviceContext* context) { - if (!g_config.ShowInfoPanel) return; - - // Init Font - if (!g_pPanelTextFormat) { - g_renderEngine->m_dwriteFactory->CreateTextFormat( - L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, - 13.0f, L"en-us", &g_pPanelTextFormat - ); - } - - // Ensure format attributes are reset (prevent contamination from DrawCompactInfo) - if (g_pPanelTextFormat) { - g_pPanelTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); - g_pPanelTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); - g_pPanelTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); - } - - // Panel Rect (Top Left) - float padding = 10.0f; - float width = 300.0f; - float height = 220.0f; - float startX = 20.0f; - float startY = 40.0f; - - if (g_currentMetadata.HasGPS) height += 50.0f; - if (g_config.InfoPanelExpanded && !g_currentMetadata.HistL.empty()) height += 100.0f; - if (!g_currentMetadata.Software.empty()) height += 20.0f; - - D2D1_RECT_F panelRect = D2D1::RectF(startX, startY, startX + width, startY + height); - - // Background - ComPtr brushBg; - context->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.7f), &brushBg); - context->FillRoundedRectangle(D2D1::RoundedRect(panelRect, 8.0f, 8.0f), brushBg.Get()); - - ComPtr brushWhite; - context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &brushWhite); - - // Buttons (Top Right) - // Close [ X ] - g_panelCloseRect = D2D1::RectF(startX + width - 25, startY + 5, startX + width - 5, startY + 25); - context->DrawTextW(L"x", 1, g_pPanelTextFormat.Get(), D2D1::RectF(g_panelCloseRect.left + 5, g_panelCloseRect.top, g_panelCloseRect.right, g_panelCloseRect.bottom), brushWhite.Get()); - - // Minimize [ - ] - g_panelToggleRect = D2D1::RectF(startX + width - 50, startY + 5, startX + width - 30, startY + 25); - context->DrawTextW(L"-", 1, g_pPanelTextFormat.Get(), D2D1::RectF(g_panelToggleRect.left + 6, g_panelToggleRect.top, g_panelToggleRect.right, g_panelToggleRect.bottom), brushWhite.Get()); - - - // === NEW: Grid-Based Info Panel === - BuildInfoGrid(); - float gridStartY = startY + 30.0f; // After buttons - float gridContentHeight = g_infoGrid.size() * GRID_ROW_HEIGHT; - DrawInfoGrid(context, startX + padding, gridStartY, width - padding * 2); - - // Calculate where histogram/GPS should start (after grid content) - - float currentY = startY + (height - (g_currentMetadata.HasGPS ? 130.0f : 100.0f)); - // Heuristic layout is tricky with variable lines. - // Re-calculating Y based on lines? - // Let's use fixed offset from bottom for Histogram? - if (!g_currentMetadata.HistR.empty()) { - float histH = 80.0f; - float histY = startY + height - padding - histH - (g_currentMetadata.HasGPS ? 50.0f : 0); - DrawHistogram(context, g_currentMetadata, D2D1::RectF(startX + padding, histY, startX + width - padding, histY + histH)); - } - - // GPS - g_gpsLinkRect = {}; - if (g_currentMetadata.HasGPS) { - float gpsY = startY + height - 55.0f; - - // Line 1: Coordinates (clickable to copy) - wchar_t gpsBuf[128]; - swprintf_s(gpsBuf, L"GPS: %.5f, %.5f", g_currentMetadata.Latitude, g_currentMetadata.Longitude); - g_gpsCoordRect = D2D1::RectF(startX + padding, gpsY, startX + width - padding, gpsY + 18.0f); - context->DrawTextW(gpsBuf, (UINT32)wcslen(gpsBuf), g_pPanelTextFormat.Get(), g_gpsCoordRect, brushWhite.Get()); - - // Line 2: Altitude and OpenMap - float line2Y = gpsY + 20.0f; - - // Altitude (if available) - if (g_currentMetadata.Altitude != 0) { - wchar_t altBuf[64]; swprintf_s(altBuf, L"Alt: %.1fm", g_currentMetadata.Altitude); - D2D1_RECT_F altRect = D2D1::RectF(startX + padding, line2Y, startX + width - 90, line2Y + 18.0f); - context->DrawTextW(altBuf, (UINT32)wcslen(altBuf), g_pPanelTextFormat.Get(), altRect, brushWhite.Get()); - } - - // Link Button - Right side - g_gpsLinkRect = D2D1::RectF(startX + width - 85.0f, line2Y, startX + width - padding, line2Y + 18.0f); - ComPtr brushLink; - context->CreateSolidColorBrush(D2D1::ColorF(0.4f, 0.7f, 1.0f), &brushLink); - context->DrawTextW(L"OpenMap", 7, g_pPanelTextFormat.Get(), g_gpsLinkRect, brushLink.Get()); - } -} - - - -void OnPaint(HWND hwnd) { - if (!g_renderEngine) return; - - // Track mouse position and update grid hover state - POINT cursorPos; - GetCursorPos(&cursorPos); - ScreenToClient(hwnd, &cursorPos); - g_lastMousePos = cursorPos; - - // Check which grid row is being hovered - g_hoverRowIndex = -1; - if (g_config.InfoPanelExpanded && g_config.ShowInfoPanel) { - float x = (float)cursorPos.x; - float y = (float)cursorPos.y; - for (size_t i = 0; i < g_infoGrid.size(); i++) { - const auto& row = g_infoGrid[i]; - if (x >= row.hitRect.left && x <= row.hitRect.right && - y >= row.hitRect.top && y <= row.hitRect.bottom) { - g_hoverRowIndex = (int)i; - break; - } - } - } - - g_renderEngine->BeginDraw(); - auto context = g_renderEngine->GetDeviceContext(); - if (context) { - context->SetTransform(D2D1::Matrix3x2F::Identity()); - context->Clear(D2D1::ColorF(0.18f, 0.18f, 0.18f)); - - if (g_currentBitmap) { - D2D1_SIZE_F size = g_currentBitmap->GetSize(); - D2D1_SIZE_F rtSize = context->GetSize(); - - // Calculate fit scale (to fit image in window at Zoom=1.0) - float fitScale = std::min(rtSize.width / size.width, rtSize.height / size.height); - float finalScale = fitScale * g_viewState.Zoom; - - // Calculate centering offset based on FINAL scaled size - float scaledW = size.width * finalScale; - float scaledH = size.height * finalScale; - float offsetX = (rtSize.width - scaledW) / 2.0f; - float offsetY = (rtSize.height - scaledH) / 2.0f; - - // Apply pan offset - float totalX = offsetX + g_viewState.PanX; - float totalY = offsetY + g_viewState.PanY; - - D2D1::Matrix3x2F transform = D2D1::Matrix3x2F::Scale(finalScale, finalScale, D2D1::Point2F(0, 0)) - * D2D1::Matrix3x2F::Translation(totalX, totalY); - - context->SetTransform(transform); - - // Draw at (0,0) with original size, transform handles placement - // Use dynamic interpolation: LINEAR during interaction for responsiveness, CUBIC when static for quality - D2D1_INTERPOLATION_MODE interpMode = g_viewState.IsInteracting - ? D2D1_INTERPOLATION_MODE_LINEAR - : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; - context->DrawBitmap(g_currentBitmap.Get(), D2D1::RectF(0, 0, size.width, size.height), 1.0f, interpMode); - } - - // Reset transform for OSD and UI elements - context->SetTransform(D2D1::Matrix3x2F::Identity()); - - RECT rect; GetClientRect(hwnd, &rect); - D2D1_SIZE_F size = context->GetSize(); - CalculateWindowControls(size); - DrawWindowControls(hwnd, context); - - // Draw OSD - g_renderEngine->DrawOSD(g_osd); - - // Draw Info Panel - // Draw Info Panel OR Compact Overlay - if (g_config.ShowInfoPanel) { - if (g_config.InfoPanelExpanded) { - DrawInfoPanel(context); - } else { - DrawCompactInfo(context); - } - } - - // Draw Tooltip (from grid hover) - DrawGridTooltip(context); - - // --- Gallery Overlay --- - g_gallery.Update(0.016f); - if (g_gallery.IsVisible()) { - D2D1_SIZE_F rtSize = context->GetSize(); - g_gallery.Render(context, rtSize); - } - - // Toolbar (Top of Gallery?) - g_toolbar.Render(context); - - DrawDialog(context, rect); - } - g_renderEngine->EndDraw(); - g_renderEngine->Present(); - - ValidateRect(hwnd, nullptr); -} diff --git a/QuickView/pch.cpp b/QuickView/pch.cpp deleted file mode 100644 index 1d9f38c5..00000000 --- a/QuickView/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "pch.h" diff --git a/QuickView/pch.h b/QuickView/pch.h deleted file mode 100644 index 21a07723..00000000 --- a/QuickView/pch.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -// Windows headers -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#include - -// Direct2D and DirectWrite -#include -#include -#include -#include -#include - -// COM smart pointers -#include -using Microsoft::WRL::ComPtr; - -// C++ Standard Library -#include -#include -#include -#include - -// Pragmas for linking -#pragma comment(lib, "d2d1.lib") -#pragma comment(lib, "d3d11.lib") -#pragma comment(lib, "dxgi.lib") -#pragma comment(lib, "dwrite.lib") -#pragma comment(lib, "windowscodecs.lib") - -// Helper macro for HRESULT checking -#ifndef THROW_IF_FAILED -#define THROW_IF_FAILED(hr) \ - if (FAILED(hr)) { throw std::runtime_error("HRESULT failed: " + std::to_string(hr)); } -#endif diff --git a/QuickView/revert_error.txt b/QuickView/revert_error.txt deleted file mode 100644 index 538c7f87..00000000 --- a/QuickView/revert_error.txt +++ /dev/null @@ -1,44 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework -Build started 2025/12/21 13:23:00. - - 1>Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" on node 1 (Build target(s)). - 1>PrepareForBuild: - Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. - InitializeBuildStatus: - Touching "E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.tlog\unsuccessfulbuild". - ComputeStdModulesCompileInputs: - Reading library manifest file 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\modules.json' which provides data for library 'microsoft/STL'. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.ixx' from library 'microsoft/STL' to be built with this project. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.compat.ixx' from library 'microsoft/STL' to be built with this project. - VcpkgTripletSelection: - Using triplet "x64-windows-static-opt" from "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\" - Using normalized configuration "Release" - ClCompile: - All outputs are up-to-date. - All outputs are up-to-date. - All outputs are up-to-date. - Link: - D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\bin\HostX86\x64\link.exe /ERRORREPORT:QUEUE /OUT:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe" /NOLOGO /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib" /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\manual-link" dwrite.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\*.lib" /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.pdb" /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /LTCG:incremental /LTCGOUT:"E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.iobj" /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib" /MACHINE:X64 E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\pch.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\RenderEngine.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\ImageLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\LosslessTransform.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\StbLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\TinyExrLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\WuffsImpl.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\main.obj - ÕýÔÚ´´½¨¿â E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib ºÍ¶ÔÏó E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exp - 1>StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_malloc [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>Done Building Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target(s)) -- FAILED. - -Build FAILED. - - "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target) (1) -> - (Link target) -> - StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_malloc [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - - 0 Warning(s) - 2 Error(s) - -Time Elapsed 00:00:01.30 diff --git a/QuickView/shim_error.txt b/QuickView/shim_error.txt deleted file mode 100644 index 73fd2a0c..00000000 --- a/QuickView/shim_error.txt +++ /dev/null @@ -1,44 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework -Build started 2025/12/21 13:28:03. - - 1>Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" on node 1 (Build target(s)). - 1>PrepareForBuild: - Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. - InitializeBuildStatus: - Touching "E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.tlog\unsuccessfulbuild". - ComputeStdModulesCompileInputs: - Reading library manifest file 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\modules.json' which provides data for library 'microsoft/STL'. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.ixx' from library 'microsoft/STL' to be built with this project. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.compat.ixx' from library 'microsoft/STL' to be built with this project. - VcpkgTripletSelection: - Using triplet "x64-windows-static-opt" from "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\" - Using normalized configuration "Release" - ClCompile: - All outputs are up-to-date. - All outputs are up-to-date. - All outputs are up-to-date. - Link: - D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\bin\HostX86\x64\link.exe /ERRORREPORT:QUEUE /OUT:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe" /NOLOGO /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib" /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\manual-link" dwrite.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\*.lib" /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.pdb" /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /LTCG:incremental /LTCGOUT:"E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.iobj" /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib" /MACHINE:X64 E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\pch.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\RenderEngine.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\ImageLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\LosslessTransform.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\StbLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\TinyExrLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\WuffsImpl.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\main.obj - ÕýÔÚ´´½¨¿â E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib ºÍ¶ÔÏó E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exp - 1>StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_malloc [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>Done Building Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target(s)) -- FAILED. - -Build FAILED. - - "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target) (1) -> - (Link target) -> - StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_malloc [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - - 0 Warning(s) - 2 Error(s) - -Time Elapsed 00:00:01.21 diff --git a/QuickView/targetver.h b/QuickView/targetver.h deleted file mode 100644 index eae4b4fb..00000000 --- a/QuickView/targetver.h +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -#include diff --git a/QuickView/wrapper_error.txt b/QuickView/wrapper_error.txt deleted file mode 100644 index a8ebebd4..00000000 --- a/QuickView/wrapper_error.txt +++ /dev/null @@ -1,44 +0,0 @@ -MSBuild version 18.0.5+e22287bf1 for .NET Framework -Build started 2025/12/21 13:26:52. - - 1>Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" on node 1 (Build target(s)). - 1>PrepareForBuild: - Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. - InitializeBuildStatus: - Touching "E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.tlog\unsuccessfulbuild". - ComputeStdModulesCompileInputs: - Reading library manifest file 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\modules.json' which provides data for library 'microsoft/STL'. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.ixx' from library 'microsoft/STL' to be built with this project. - Adding module source 'D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\modules\std.compat.ixx' from library 'microsoft/STL' to be built with this project. - VcpkgTripletSelection: - Using triplet "x64-windows-static-opt" from "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\" - Using normalized configuration "Release" - ClCompile: - All outputs are up-to-date. - All outputs are up-to-date. - All outputs are up-to-date. - Link: - D:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\bin\HostX86\x64\link.exe /ERRORREPORT:QUEUE /OUT:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe" /NOLOGO /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib" /LIBPATH:"E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\manual-link" dwrite.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib "E:\Personal\Coding\QuickView\third_party\vcpkg\installed\x64-windows-static-opt\lib\*.lib" /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.pdb" /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /LTCG:incremental /LTCGOUT:"E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\QuickView2026.iobj" /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib" /MACHINE:X64 E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\pch.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\RenderEngine.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\ImageLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\LosslessTransform.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\StbLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\TinyExrLoader.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\WuffsImpl.obj - E:\Personal\Coding\QuickView\QuickView2026\obj\x64\Release\main.obj - ÕýÔÚ´´½¨¿â E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.lib ºÍ¶ÔÏó E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exp - 1>StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_malloc [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - 1>Done Building Project "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target(s)) -- FAILED. - -Build FAILED. - - "E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj" (Build target) (1) -> - (Link target) -> - StbLoader.obj : error LNK2001: ÎÞ·¨½âÎöµÄÍⲿ·ûºÅ stbi_zlib_decode_malloc [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - E:\Personal\Coding\QuickView\QuickView2026\bin\x64\Release\QuickView2026.exe : fatal error LNK1120: 1 ¸öÎÞ·¨½âÎöµÄÍⲿÃüÁî [E:\Personal\Coding\QuickView\QuickView2026\QuickView2026.vcxproj] - - 0 Warning(s) - 2 Error(s) - -Time Elapsed 00:00:01.30 diff --git a/custom-ports/libpng/cmake.patch b/custom-ports/libpng/cmake.patch deleted file mode 100644 index ebac200f..00000000 --- a/custom-ports/libpng/cmake.patch +++ /dev/null @@ -1,92 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index d91e3a6c3..249d04c0f 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -144,6 +144,12 @@ set(PNG_LINK_LIBRARIES ZLIB::ZLIB) - # Find the math library (unless we already know it's not available or - # not needed). - if(UNIX AND NOT (APPLE OR BEOS OR HAIKU OR EMSCRIPTEN)) -+ block(SCOPE_FOR VARIABLES) -+ if(VCPKG_CRT_LINKAGE STREQUAL "static") -+ list(PREPEND CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_STATIC_LIBRARY_SUFFIX}") -+ endif() -+ check_library_exists(m pow "${CMAKE_C_IMPLICIT_LINK_DIRECTORIES}" PNG_HAVE_LIBM_POW) -+ endblock() - check_library_exists(m pow "" PNG_HAVE_LIBM_POW) - endif() - if(PNG_HAVE_LIBM_POW) -@@ -240,9 +246,9 @@ if(PNG_HARDWARE_OPTIMIZATIONS) - - # Set definitions and sources for MIPS. - if(PNG_TARGET_ARCHITECTURE MATCHES "^(mipsel|mips64el)") -- set(PNG_MIPS_MSA_POSSIBLE_VALUES on off) -+ set(PNG_MIPS_MSA_POSSIBLE_VALUES on off check) - set(PNG_MIPS_MSA -- "on" -+ "check" - CACHE STRING "Enable MIPS_MSA optimizations: on|off; on is default") - set_property(CACHE PNG_MIPS_MSA - PROPERTY STRINGS ${PNG_MIPS_MSA_POSSIBLE_VALUES}) -@@ -269,6 +275,12 @@ if(PNG_HARDWARE_OPTIMIZATIONS) - mips/filter_mmi_inline_assembly.c) - add_definitions(-DPNG_MIPS_MSA_OPT=2) - add_definitions(-DPNG_MIPS_MMI_OPT=1) -+ elseif(PNG_MIPS_MSA STREQUAL "check") -+ set(libpng_mips_sources -+ mips/mips_init.c -+ mips/filter_msa_intrinsics.c) -+ add_definitions(-DPNG_MIPS_MSA_CHECK_SUPPORTED) -+ add_definitions(-DPNG_MIPS_MMI_CHECK_SUPPORTED) - elseif(PNG_MIPS_MSA STREQUAL "on") - set(libpng_mips_sources - mips/mips_init.c -@@ -661,7 +673,7 @@ else() - # We also need to use a custom suffix, in order to distinguish between the - # shared import library name and the static library name. - set(PNG_SHARED_OUTPUT_NAME "libpng${PNGLIB_ABI_VERSION}") -- set(PNG_STATIC_OUTPUT_NAME "libpng${PNGLIB_ABI_VERSION}_static") -+ set(PNG_STATIC_OUTPUT_NAME "libpng${PNGLIB_ABI_VERSION}") - endif() - - if(PNG_SHARED) -@@ -939,10 +951,10 @@ if(PNG_TESTS AND PNG_SHARED) - FILES ${PNGSUITE_PNGS}) - endif() - --if(PNG_SHARED AND PNG_TOOLS) -+if(PNG_TOOLS) - add_executable(pngfix ${pngfix_sources}) - target_link_libraries(pngfix -- PRIVATE png_shared) -+ PRIVATE $ $) # in vcpkg there's only one - set(PNG_BIN_TARGETS pngfix) - - add_executable(png-fix-itxt ${png_fix_itxt_sources}) -@@ -1028,12 +1040,15 @@ endif() - # We use the same files like ./configure, so we have to set its vars. - # Only do this on Windows for Cygwin - the files don't make much sense - # outside of a UNIX look-alike. --if(NOT WIN32 OR CYGWIN OR MINGW) -+if(1) - set(prefix "${CMAKE_INSTALL_PREFIX}") - set(exec_prefix "${CMAKE_INSTALL_PREFIX}") - set(libdir "${CMAKE_INSTALL_FULL_LIBDIR}") - set(includedir "${CMAKE_INSTALL_FULL_INCLUDEDIR}") -- set(LIBS "-lz -lm") -+ set(LIBS "") -+ if(PNG_HAVE_LIBM_POW) -+ string(APPEND LIBS "-lm") -+ endif() - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/libpng.pc.in" - "${CMAKE_CURRENT_BINARY_DIR}/libpng${PNGLIB_ABI_VERSION}.pc" - @ONLY) -@@ -1092,6 +1107,9 @@ if(NOT SKIP_INSTALL_PROGRAMS AND NOT SKIP_INSTALL_ALL) - endif() - - if(NOT SKIP_INSTALL_FILES AND NOT SKIP_INSTALL_ALL) -+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libpng${PNGLIB_ABI_VERSION}.pc -+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) -+elseif(0) - # Install the man pages. - install(FILES libpng.3 libpngpf.3 - DESTINATION "${CMAKE_INSTALL_MANDIR}/man3") diff --git a/custom-ports/libpng/libpng-config.cmake b/custom-ports/libpng/libpng-config.cmake deleted file mode 100644 index a2644487..00000000 --- a/custom-ports/libpng/libpng-config.cmake +++ /dev/null @@ -1,12 +0,0 @@ -file(READ "${CMAKE_CURRENT_LIST_DIR}/usage" usage) -message(WARNING "find_package(libpng) is deprecated.\n${usage}") - -include(CMakeFindDependencyMacro) -find_dependency(PNG CONFIG) - -if(NOT TARGET png_shared) - add_library(png_shared ALIAS PNG::PNG) -endif() -if(NOT TARGET png_static) - add_library(png_static ALIAS PNG::PNG) -endif() diff --git a/custom-ports/libpng/portfile.cmake b/custom-ports/libpng/portfile.cmake deleted file mode 100644 index 9ed26484..00000000 --- a/custom-ports/libpng/portfile.cmake +++ /dev/null @@ -1,128 +0,0 @@ -# Download the apng patch -set(LIBPNG_APNG_PATCH_PATH "") -if ("apng" IN_LIST FEATURES) - if(VCPKG_HOST_IS_WINDOWS) - # Get (g)awk and gzip installed - vcpkg_acquire_msys(MSYS_ROOT PACKAGES gawk gzip) - set(AWK_EXE_PATH "${MSYS_ROOT}/usr/bin") - vcpkg_add_to_path("${AWK_EXE_PATH}") - endif() - - set(LIBPNG_APNG_PATCH_NAME "libpng-${VERSION}-apng.patch") - vcpkg_download_distfile(LIBPNG_APNG_PATCH_ARCHIVE - URLS "https://downloads.sourceforge.net/project/libpng-apng/libpng16/${VERSION}/${LIBPNG_APNG_PATCH_NAME}.gz" - FILENAME "${LIBPNG_APNG_PATCH_NAME}.gz" - SHA512 9da270cb434fc00e16e66535c955e283367b7bf9ae64292b3fa3921e03d101b2c800041db23687e8653e2a2db1be126497d9c5bfd74cbe626bf2ad7d6f706978 - ) - set(LIBPNG_APNG_PATCH_PATH "${CURRENT_BUILDTREES_DIR}/src/${LIBPNG_APNG_PATCH_NAME}") - if (NOT EXISTS "${LIBPNG_APNG_PATCH_PATH}") - file(INSTALL "${LIBPNG_APNG_PATCH_ARCHIVE}" DESTINATION "${CURRENT_BUILDTREES_DIR}/src") - vcpkg_execute_required_process( - COMMAND gzip -d "${LIBPNG_APNG_PATCH_NAME}.gz" - WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/src" - ALLOW_IN_DOWNLOAD_MODE - LOGNAME extract-patch.log - ) - endif() -endif() - -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO pnggroup/libpng - REF v${VERSION} - SHA512 e854589401043c5f24e0365a578ccda8fde47766fa5990c7355581e4f0dc67bfefd7f1272fc25c269968135826cf583b1a10d01667af97e2ec4410cfaf2a700c - HEAD_REF master - PATCHES - "${LIBPNG_APNG_PATCH_PATH}" - cmake.patch -) - -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" PNG_SHARED) -string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "static" PNG_STATIC) - -vcpkg_check_features( - OUT_FEATURE_OPTIONS FEATURE_OPTIONS - FEATURES - tools PNG_TOOLS - INVERTED_FEATURES - tools SKIP_INSTALL_PROGRAMS -) - -vcpkg_list(SET LIBPNG_HARDWARE_OPTIMIZATIONS_OPTION) -if(VCPKG_TARGET_IS_IOS) - vcpkg_list(APPEND LIBPNG_HARDWARE_OPTIMIZATIONS_OPTION "-DPNG_HARDWARE_OPTIMIZATIONS=OFF") -endif() - -vcpkg_list(SET LD_VERSION_SCRIPT_OPTION) -if(VCPKG_TARGET_IS_ANDROID) - vcpkg_list(APPEND LD_VERSION_SCRIPT_OPTION "-Dld-version-script=OFF") - if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") - vcpkg_cmake_get_vars(cmake_vars_file) - include("${cmake_vars_file}") - if(VCPKG_DETECTED_CMAKE_ANDROID_ARM_NEON) - vcpkg_list(APPEND LIBPNG_HARDWARE_OPTIMIZATIONS_OPTION "-DPNG_ARM_NEON=on") - else() - # for armeabi-v7a, check whether NEON is available - vcpkg_list(APPEND LIBPNG_HARDWARE_OPTIMIZATIONS_OPTION "-DPNG_ARM_NEON=check") - endif() - endif() -endif() - -if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64" AND VCPKG_TARGET_IS_LINUX) - vcpkg_list(APPEND LIBPNG_HARDWARE_OPTIMIZATIONS_OPTION "-DPNG_ARM_NEON=on") -endif() - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - OPTIONS - "-DZLIB_INCLUDE_DIR=${CURRENT_INSTALLED_DIR}/include" - "-DZLIB_LIBRARY_RELEASE=${CURRENT_INSTALLED_DIR}/lib/zlibstatic-ng.lib" - "-DZLIB_LIBRARY_DEBUG=${CURRENT_INSTALLED_DIR}/debug/lib/zlibstatic-ngd.lib" - # CMake FindZLIB typically uses ZLIB_LIBRARY for the main one, but separate release/debug might require CMAKE_ flags or specific handling. - # But for FindZLIB.cmake provided by CMake, usually ZLIB_LIBRARY is sufficient if generic? - # Let's try setting standard CMake cache entries. - "-DZLIB_LIBRARY=${CURRENT_INSTALLED_DIR}/lib/zlibstatic-ng.lib" - ${LIBPNG_HARDWARE_OPTIMIZATIONS_OPTION} - ${LD_VERSION_SCRIPT_OPTION} - -DPNG_STATIC=${PNG_STATIC} - -DPNG_SHARED=${PNG_SHARED} - -DPNG_FRAMEWORK=OFF - -DPNG_TESTS=OFF - -DSKIP_INSTALL_EXECUTABLES=ON - -DSKIP_INSTALL_FILES=OFF - ${FEATURE_OPTIONS} - OPTIONS_DEBUG - -DSKIP_INSTALL_HEADERS=ON - MAYBE_UNUSED_VARIABLES - PNG_ARM_NEON -) -vcpkg_cmake_install() -vcpkg_cmake_config_fixup(PACKAGE_NAME png CONFIG_PATH lib/cmake/PNG) -vcpkg_cmake_config_fixup(CONFIG_PATH lib/libpng) -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/png") - -# unofficial legacy usage -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/libpng-config.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") - -vcpkg_fixup_pkgconfig() -if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) - if(NOT VCPKG_BUILD_TYPE) - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libpng16.pc" "-lpng16" "-llibpng16d") - file(INSTALL "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libpng16.pc" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig" RENAME "libpng.pc") - endif() - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/libpng16.pc" "-lpng16" "-llibpng16") -elseif(NOT VCPKG_BUILD_TYPE) - vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libpng16.pc" "-lpng16" "-lpng16d") - file(INSTALL "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/libpng16.pc" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig" RENAME "libpng.pc") -endif() -file(INSTALL "${CURRENT_PACKAGES_DIR}/lib/pkgconfig/libpng16.pc" DESTINATION "${CURRENT_PACKAGES_DIR}/lib/pkgconfig" RENAME "libpng.pc") - -vcpkg_copy_pdbs() - -if(PNG_TOOLS) - vcpkg_copy_tools(TOOL_NAMES "pngfix" "png-fix-itxt" AUTO_CLEAN) -endif() - -file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/custom-ports/libpng/usage b/custom-ports/libpng/usage deleted file mode 100644 index 122bb991..00000000 --- a/custom-ports/libpng/usage +++ /dev/null @@ -1,4 +0,0 @@ -libpng provides CMake targets: - - find_package(PNG REQUIRED) - target_link_libraries(main PRIVATE PNG::PNG) diff --git a/custom-ports/libpng/vcpkg-cmake-wrapper.cmake b/custom-ports/libpng/vcpkg-cmake-wrapper.cmake deleted file mode 100644 index de1e087f..00000000 --- a/custom-ports/libpng/vcpkg-cmake-wrapper.cmake +++ /dev/null @@ -1,3 +0,0 @@ -find_library(PNG_LIBRARY_RELEASE NAMES png16 libpng16 NAMES_PER_DIR PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib" NO_DEFAULT_PATH) -find_library(PNG_LIBRARY_DEBUG NAMES png16d libpng16d NAMES_PER_DIR PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib" NO_DEFAULT_PATH) -_find_package(${ARGS}) diff --git a/custom-ports/libpng/vcpkg.json b/custom-ports/libpng/vcpkg.json deleted file mode 100644 index 0d8f7cf5..00000000 --- a/custom-ports/libpng/vcpkg.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "libpng", - "version": "1.6.53", - "description": "libpng is a library implementing an interface for reading and writing PNG (Portable Network Graphics) format files", - "homepage": "https://github.com/pnggroup/libpng", - "license": "libpng-2.0", - "dependencies": [ - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - }, - { - "name": "vcpkg-cmake-get-vars", - "host": true, - "platform": "arm & android" - }, - "zlib-ng" - ], - "features": { - "apng": { - "description": "This is backward compatible with the regular libpng, both in library usage and format" - }, - "tools": { - "description": "Build the libpng tools", - "supports": "!android & !ios" - } - } -} \ No newline at end of file diff --git a/custom-ports/zlib/portfile.cmake b/custom-ports/zlib/portfile.cmake deleted file mode 100644 index e94b758d..00000000 --- a/custom-ports/zlib/portfile.cmake +++ /dev/null @@ -1,5 +0,0 @@ -# zlib -> zlib-ng redirect overlay port -# This port redirects zlib dependency to zlib-ng with ZLIB_COMPAT mode -# zlib-ng provides faster SIMD-optimized compression/decompression - -set(VCPKG_POLICY_EMPTY_PACKAGE enabled) diff --git a/custom-ports/zlib/vcpkg.json b/custom-ports/zlib/vcpkg.json deleted file mode 100644 index 43e9ea2d..00000000 --- a/custom-ports/zlib/vcpkg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "zlib", - "version": "1.3.1", - "port-version": 1, - "description": "Redirect to zlib-ng (faster, SIMD-optimized)", - "homepage": "https://github.com/zlib-ng/zlib-ng", - "license": "Zlib", - "dependencies": [ - { - "name": "zlib-ng", - "version>=": "2.0.0" - } - ] -} \ No newline at end of file From 1460344dba58b892b3a69b9e51ea8a93658968e6 Mon Sep 17 00:00:00 2001 From: Vivor Loong Date: Tue, 23 Dec 2025 12:08:27 +0800 Subject: [PATCH 2/3] Cleanup 1.x: Removed root files --- QuickView.ico | Bin 112022 -> 0 bytes readme.html | 1317 ------------------------------------------------- 2 files changed, 1317 deletions(-) delete mode 100644 QuickView.ico delete mode 100644 readme.html diff --git a/QuickView.ico b/QuickView.ico deleted file mode 100644 index c3a5137b2915a2578e229ba2c80c614732b8b009..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112022 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%0Ae#wq)+i86Q+1cBUd5pScqj!jjHp~tsbIsv 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 z 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

3{gJ591HD-0I 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 - - - - - Readme for JPEGView - - - - - - -

- -
-
- - -

Introduction

- JPEGView is a viewer/editor for JPEG, BMP, WEBP, PNG, TGA, RAW, GIF and TIFF images. It is designed to view images from digital cameras - full screen and with highest quality possible. Basic image editing functionality is provided - allowing to correct typical - problems as color cast, high/low contrast and under- or overexposure very quickly and interactively during review of the - images. JPEGView does not replace a full blown image editor - such an editor will just be needed less often.
- Features: -
    -
  • - Supported image formats -
      -
    • JPEG (including EXIF)
    • -
    • BMP (Windows Bitmap)
    • -
    • WEBP
    • -
    • - JPEG XR (Windows XP: WIC must be installed) -
    • -
    • PNG
    • -
    • GIF (including animated GIFs)
    • -
    • TIFF
    • -
    • TGA
    • -
    • - Camera RAW (embedded JPEGs, see here for details)
      -
    • -
    -
  • -
  • - Fullscreen viewing -
      -
    • Image centric - nothing else visible by default
    • -
    • Multiple monitor support
    • -
    • Window mode also supported (Toggle with Ctrl-W)
    • -
    -
  • -
  • - Small and fast -
      -
    • Executable is only 1 MB
    • -
    • C++/MMX/SSE/AVX2 code
    • -
    • 32 bit and 64 bit versions
    • -
    • Support for multiple CPU cores to speedup processing
    • -
    • Minimal dependencies on external libraries
    • -
    -
  • -
  • - High quality resizing of images -
      -
    • Downsampling filter preserves sharpness
    • -
    • Bicubic interpolation for upsampling
    • -
    -
  • -
  • - Interactive image processing and improvement -
      -
    • Real time and full screen image processing
    • -
    • Contrast, Brightness and Saturation adaptation
    • -
    • Unsharp masking
    • -
    • Cyan-Red, Magenta-Green, Yellow-Blue color balance
    • -
    • Local brightness correction to lighten shadows and darken highlights
    • -
    • Automatic color and contrast correction
    • -
    • Rotation in 90 deg steps
    • -
    • Free image rotation for horizon correction
    • -
    • Perspective correction
    • -
    • Lossless JPEG transformations
    • -
    • Crop image (supporting lossless crop for JPEGs)
    • -
    • Landscape picture improvement mode
    • -
    -
  • -
  • - Viewing and image processing parameters can be saved per image -
      -
    • Image file remains untouched
    • -
    • Database entries remain valid when image file is moved or renamed
    • -
    -
  • -
  • - Slide shows and movie playing -
      -
    • Plays slide shows given by text file
    • -
    • Multiple transition effects for slide shows
    • -
    • - Plays all images in folder as movie with configurable frame rate.
      - All image processing remains functional while playing. -
    • -
    -
  • -
  • - Batch rename and copy -
      -
    • Copies and renames files from digicam to target folders
    • -
    • Creates target folders as needed
    • -
    • Supports various placeholders for renaming files
    • -
    -
  • -
  • - Extensible and highly configurable - -
  • -
- - -

Installation

- There is no installer for JPEGView because none is needed. Just copy or extract all files to a folder - on your harddisk (e.g. C:\Program Files\JPEGView\).
- To define JPEGView as your default viewer for image files, perform the following steps: -
    -
  • Open the context menu of JPEGView (right mouse click)
  • -
  • On the context menu, choose 'Settings/Administration' - 'Set as default viewer'
  • -
  • Windows XP - Windows 8.1:
  • -
      -
    • - On the dialog that is appearing, the file extensions of the supported image file formats are listed, - together with the information if the corresponding extension is currently bound to JPEGView. -
    • -
    • Select the file extensions to be opened by JPEGView and confirm with OK
    • -
    -
  • - Windows 10 insists on using the 'Standard-apps' dialog of the operating system to change the standard application for images.
    - Thus you need to go to the 'Standard-Apps' settings page of Windows 10: -
  • -
      -
    • Open the Windows 10 start menu and type 'standard apps'. Click the found settings page.
    • -
    • On the right panel, scroll to the end and select 'Set defaults by app'
    • -
    • In the dialog that is appearing, choose JPEGView in the list on the left panel
    • -
    • Now you can assign all or a subset of the image file types to JPEGView
    • -
    -
- Hardware Requirements
-
    -
  • - 256 MB RAM
    - 512 MB RAM is required if images with more than 10 MegaPixels are viewed, - less than 256 MB is acceptable if only small images (less than 5 MegaPixels) are viewed. -
  • -
  • - CPU should be SSE2 capable (perfect is a CPU with 128 bit SSE2 processing and multiple CPU cores)
    - Other CPUs also work but performance decreases somewhat -
  • -
  • A mouse with a scroll wheel (for zooming) and navigation buttons on the side is highly recommended
  • -
  • - If you are using JPEGView on a touch-screen: You may increase the size of the buttons on the navigation - panel by increasing the value of 'ScaleFactorNavPanel' in the INI file. -
  • -
- Supported Operating Systems
-
    -
  • Windows 10 / 8.1 / 8
  • -
  • Windows 7
  • -
  • Windows Vista
  • -
  • Windows XP (SP2 required)
  • -
  • Windows 2000 : Not supported anymore, use version 1.0.21
  • -
- Supported Languages
-
    -
  • Basque
  • -
  • Belorussian
  • -
  • Chinese
  • -
  • Czech
  • -
  • English
  • -
  • French
  • -
  • German
  • -
  • Greek
  • -
  • Italian
  • -
  • Japanese
  • -
  • Korean
  • -
  • Polish
  • -
  • Portuguese
  • -
  • Romanian
  • -
  • Russian
  • -
  • Spanish
  • -
  • Swedish
  • -
  • Ukrainian
  • -
- By default JPEGView uses the same language as the operating system. However another language can be forced using the INI file. - Translations to other languages can be done very easily by providing an UTF8 coded text mapping file, see - strings_de.txt as an example.
- If you want that your translation is included into the next JPEGView release, contact me at - jpegview@gmail.com
- Registry
- JPEGView does not touch the registry except when registering file extensions to be displayed with - JPEGView. This will add some registry entries in the 'Current User' branch of the registry.
- Created Files during Runtime
- The only location where files are written (the parameter DB and the user INI file) - is in 'Users\Username\AppData\roaming\JPEGView\'. Notice that the exact path depends on the Windows version. For exceptions of this rule see next section.
- Note that these files are only created when they are needed, e.g. when the first parameter set is stored to the parameter DB.
- Forcing user data storage in EXE path
- By setting StoreToEXEPath=true in the global INI file, - the storage location for the user INI file and the parameter DB can be changed to the EXE path of JPEGView. This makes - sense when using JPEGView from an USB memory stick to prevent storing data on the host system.
- Restricted Rights
- JPEGView runs without problems on accounts with restricted rights. Setting JPEGView as default viewer for already registered file - extensions may fail without administrator rights when using Windows Vista, Windows 7 or later. - -

Using JPEGView

- -

Getting Started

- For getting started, the following commands in JPEGView are the most important ones:
-
    -
  • - F1 displays help on JPEGView -
  • -
  • - F2 displays information about the current image -
  • -
  • - PgDn/PgUp navigates to next/previous image (Alternative: 'forward' and 'back' mouse buttons) -
  • -
  • Mouse wheel zooms in and out
  • -
  • Dragging mouse while left mouse button is pressed will pan the image
  • -
  • Move mouse pointer to bottom of screen for image processing, parameter DB access and file renaming
  • -
  • Move mouse pointer to top of screen for minimizing or closing the application
  • -
  • Right mouse button displays the context menu
  • -
  • - ESC terminates the application -
  • -
- -

Navigation Panel

- Navigation panel -
- From left to right:
- - First image in folder, Previous image, Next image, Last image in folder
- - Delete image file (to recycle bin, use Shift+Del to delete image files permanently)
- - Zoom mode, when active zooming can be done by pressing the left mouse button and dragging the mouse to the left and right
- - Actual image size / Fit image to screen
- - Window mode / Full screen mode
- - Rotate clockwise, Rotate counter-clockwise
- - Rotate image by any angle, e.g. to perform horizon correction
- - Perspecive correction, corrects tilted vertical edges
- - Keep image processing parameters and zoom/pan between images
- - Toggle landscape picture enhancement mode
- - Toggle display of image information (EXIF)
- -

Full Screen Mode / Window Mode

- By default JPEGView starts in full screen mode on the largest monitor attached to the computer. - A window mode is also supported. Toggling between full screen and window mode is done with F11 or with - the button on the navigation panel.
- By changing the INI file, it is possible to force JPEGView to start in window mode and to use a different monitor - respectively window position and size. Note that when changing the window size with the mouse, the image is not - scaled automatically to fit the new size. Press then Enter key to fit the image to the window. - - -

Viewing Images

-

Navigation

- To navigate between images, the following commands are supported: -
    -
  • - Next image
    - Keyboard: PgDn or 'Left arrow'
    - Mouse: 'Forward' mouse button (normally on left side of mouse) -
  • -
  • - Previous image
    - Keyboard: PgUp or 'Right arrow'
    - Mouse: 'Back' mouse button -
  • -
  • - First image in folder (according to sort order, see below)
    - Keyboard: Home -
  • -
  • - Last image in folder (according to sort order, see below)
    - Keyboard: End -
  • -
  • - Mark image for toggling
    - Keyboard: Ctrl-M -
  • -
  • - Toggle between current image and marked image
    - Note that toggling only works between images in the same folder
    - Keyboard: Ctrl-Left arrow or Ctrl-Right arrow -
  • -
  • - Reload image
    - Keyboard: Ctrl-R
    -
  • -
  • - Open image or folder
    - Keyboard: Ctrl-O
    - The standard 'File Open' dialog is shown and allows displaying images from other folders. - Alternatively, a text file containing file names for a slide show can be opened. -
  • -
  • - Drag and drop image or folder from Windows Explorer to JPEGView window. -
  • -
- The basic navigation command (First image in folder, Previous image, Next image, Last image in folder) are also - available on the navigation panel, transparently blended over the image.
- JPEGView uses a second thread to read ahead and process the next image. Guessing the next image is based on - the last navigations.

- - JPEGView supports the following navigation modes: -
    -
  • - F7: (Default) Display all files in current folder, wrap around to first image when finished
    - The file name of the current file and it's position in the folder can be displayed using Ctrl-F2. -
  • -
  • - F8: Display all files in current folder, when finished recursively display images in subfolders.
    - Subfolders in the same root directory are traversed in alphabetic order. -
  • -
  • - F9: Display all files in current folder, when finished advance to next sibling folder - (folder on same level as current folder) -
  • -
- - The default navigation mode can be changed in the INI file.

- The display-order within one folder is also configurable: -
    -
  • - m: (Default) Order files according to date/time of last modification -
  • -
  • - c: Order files according to date/time of creation -
  • -
  • - n: Order files according to file name -
  • -
  • - z: Random file order -
  • -
- The default ordering can be changed in the INI file.
- -

Image Information

-
Image and EXIF Information
- Use F2 to display information about the current image, including the value of some EXIF tags, e.g. date and - time the image was taken and exposure information. Optionally the histogram of the image can be displayed below the - EXIF information. The histogram is updated immediately when the image is processed. -
File Name and Path
- Use Ctrl-F2 to display the full path and file name of the current image. - -

Zoom and Pan

- Images can be zoomed and panned interactively. While zooming, a lower quality image processing mode is used. The - image will be processed automatically with the high quality resize filters as soon as no zoom command is received for - about half a second.
- By default, images that are larger than the screen are scaled down to fit the screen. Images that are smaller than the screen - are not scaled by default. Use the following commands to change the zoom factor: -
    -
  • - Fit image to screen
    - Keyboard: Enter - Mouse: Double click on the image -
  • -
  • - Fill screen with crop (no black borders)
    - Keyboard: Ctrl-Enter -
  • -
  • - Zoom to 100 % (One image pixel corresponds to one screen pixel)
    - Keyboard: Space
    - Toggles back to 'Fit to screen' when zoom is already 100 %
    - Note that the point under the mouse cursor remains fixed during zoom - allows zooming to point of interest -
  • -
  • - Enlarge view to span all monitors (only available on multi monitor systems)
    - Keyboard: F12
    - Panoramas look great in this mode if you have multiple screen.
    - Pressing F12 again toggles back to normal one-screen mode. -
  • -
  • - Free Zoom
    - Keyboard: 'Plus' or Ctrl-'Arrow up' to zoom in, 'Minus' or Ctrl-'Arrow down' to zoom out
    - Mouse: Mouse wheel or Shift-left mouse click and drag mouse to the left and right
    -
  • -
  • - Pan
    - Keyboard: Shift+'Arrows'
    - Mouse: Left mouse and drag
    - Pan is only supported when the image is larger than the screen -
  • -
-
Because almost all images from digital cameras must be sampled down for a typical monitor, the downsampling filter - has been choosen carefully to provide an optimal image quality. The downsampling filter uses a 7 x 7 to 15 x 15 - (depending on resize-factor) Lanczos-like filter kernel. This kernel produces only a small amout of aliasing while - preserving or even slightly enhancing the sharpness of the image. You can see the difference to simple point sampling when - pressing F3 what toggles between point sampling and filter based sampling. - -

Auto Scale Mode

- The auto scale mode determines how images are fitted to the screen. The default auto scale mode is read from the - INI file, however the mode can also be changed during runtime with the context menu, submenu 'Auto scale mode'.
- The following modes are supported: -
    -
  • - Fit to screen (no zoom)
    - Images that are larger than the screen are scaled down to fit. No image areas are cropped but black borders - will be visible when the image aspect ratio and the screen aspect ratio do not match. Images that are smaller - than the screen are not enlarged. -
  • -
  • - Fill with crop (no zoom)
    - Images that are larger than the screen are scaled down to fill the entire screen. Cropping occurs if necessary to - avoid black borders. Images that are smaller than the screen are not enlarged.
    - This mode is suited for viewing 4:3 images on a 16:9 wide screen without black borders.
    - Note: Cropping is limited to approximately 30 % of the image in this mode. If more would be cropped, fit to screen is used instead. - As a result, portrait images will not be cropped on the screen. -
  • -
  • - Fit to screen
    - Images are scaled (up or down) to fit the screen. No image areas are cropped but black borders can be visible. -
  • -
  • - Fill with crop
    - Images are scaled (up or down) to fill the entire screen. Unlimited cropping may occurs if necessary to avoid - black borders. -
  • -
- -

Rotate

- With the 'Arrow down' and 'Arrow up' keys, the current image can be rotated clockwise, respectively - counter-clockwise. Note that JPEGView looks for the rotation tag in the EXIF data to rotate the image automatically. Some - cameras have an orientation sensor and set this tag.
- To rotate a JPEG image lossless on disk, use the R and T keys. - -

Crop

- When the Ctrl key and the left mouse button are pressed and the mouse is dragged, a selection on the image can be - defined. After having defined a selection, a context menu provides the following commands: -
    -
  • - Crop image selection
    - Crops the image selection and re-applies all image processing with the current parameters to the cropped image. - When the color and contrast correction is active, the cropped image may looks different afterwards. Note that a crop - operation cannot be stored to the parameter DB. -
  • -
  • - Lossless crop (for JPEG images only)
    - Crops the image selection without recompression of the JPEG image. The cropped image must be stored as a new JPEG file. - Note that lossless crop cannot be more exact than to 8 or 16 pixel boundaries due to the structure of JPEG images. -
  • -
  • - Copy image selection to clipboard
    - Note that this command will copy the image selection in original resolution and not in screen resolution. -
  • -
  • - Crop Mode
    - Sets the crop mode - either to free croping, fixed size cropping or cropping with a fixed aspect ratio. - Note that the default fixed crop size can be set in the INI file. -
  • -
  • Zoom to selection
  • -
- Hint: Use a large zoom factor and the point-sampling mode (F3 key) to crop with pixel precision.
- Note: When the crop-menu appears, the crop rectangle can still be edited by clicking on the handles or on the border of - the rectangle. Use the right mouse button to display the crop-menu again when the crop rectangle has the desired size. - - -

Lossless JPEG Transformations

- The JPEG compression scheme consists of multiple steps. Some of the steps are lossless, others are lossy. Because of the - lossy steps, each editing operation involving a recompression results in a (moderate) quality decrease. Lossless JPEG - transformations do not include any of the lossy compression steps and therefore should be prefered whenever possible. - However only a very limited number of operations can be done lossless on JPEG images: -
    -
  • - Rotation in steps of 90 degrees -
  • -
  • - Mirror horizontally and vertically -
  • -
  • - Crop -
  • -
- Note: Due to the structure of JPEG files, all these transformations only work if the width and height of the JPEG image is a multiple of the JPEG compression - block size. This block size is 8 or 16 pixels, depending on the chrominance subsampling used for the image. - -

Slide Shows

- JPEGView can display the content of the current folder (or folders if the navigation mode is set accordingly) as a slide show. - Alternatively a text file can be used to list the image files being part of a slide show. -

Slide Show Text File

- A text file containing file names can be passed on the command line to JPEGView. Alternatively such a file can be loaded - using the 'Open image' command. The following text file formats are supported: -
    -
  • ANSI coded text file
  • -
  • Unicode (UTF16) text file
  • -
  • UTF8 coded text file
  • -
- All text formats must contain one file name per line. The file names must use paths relative to the folder of the slide show - text file or absolute paths and must be terminated either with a hyphen ("), with a newline character or with a < character.
- The following examples show possible line formats:
- - d:\Images\test.jpg
- test.jpg
- "d:\My Documents\test.jpg"
- <image>d:\My Documents\test2.jpg</image>
-
- Note that the parser searches for a path marker (looking for :\ or \\) and ignores all text before the first found - path marker. Starting from the path marker, the parser searches forward for an end marker (newline, " or <) and - everything after that end marker until end of line is also ignored.
- A slide show file list is a sort of virtual folder containing all images listed in the slide show file. - -

Using Slide Shows

- The following commands can be used to control a slide show: -
    -
  • - PgDn/PgUp/Home/End : Navigates to next/previous/first/last image of slide show -
  • -
  • - 1 .. 9 : Starts the slide show with a delay of n seconds between the images -
  • -
  • - Ctrl + [Shift] + 1 .. 9 : Uses a delay of n/10 of a second, respectively n/100 of a second -
  • -
  • - Esc : Stops the slide show -
  • -
- If the current context is a folder and not a slide show list, the slideshow is performed with the images - in the folder. The folder navigation mode and the sorting order are taken into account in this case - with - a slide show list they have no effect. For showing the images in a folder in random order, press 'z' before - starting the slide show (this sets random sorting order). -

Transition Effects

- Different transition effects can be used to switch between images when playing a slide show. - The use of transition effects has the following limitations: -
    -
  • - Transition effects are only available in full screen mode -
  • -
  • - When playing a slide show with transition effects, reaction on commands may be somewhat slow. - The ESC key always immediatly terminates the slide show. -
  • -
- Use the INI file to set the default transition effect and the default transition effect speed. - -

Playing Image Series as Movie

- The slide show feature can be used to play a series of images in a folder or given by a text file as a movie: -
    -
  • - First set the display order for the files. The order is either given by a text file or by choosing the - display order mode for folders accordingly. -
  • -
  • - Press the Home key to go to the first frame of the movie. -
  • -
  • - Start the movie using the context menu 'Play folder as movie' or the Ctrl+[Shift]+Number keys. - Of course the number of frames per seconds is limited by the computing power available to decode and process the frames. - JPEGView will never skip frames - if more - frames are requested than possible, the frames are displayed as fast as possible. During movie playing, high quality - resampling, contrast correction and local brightness correction are disabled to speed up processing. -
  • -
  • Keeping parameters between the frames is enabled during movie playing - corrections apply to the whole movie.
  • -
  • - To stop the movie, press the Esc key. -
  • -
- -

Processing Images

- JPEGView provides some basic image processing. Most image processing is done interactively on the fullscreen image what - allows to see the effect of the processing immediately. The processed image can be stored either by storing the - applied parameters in the parameter DB (the image remains untouched), by copying the processed image to the clipboard to - further process it with an external program or by saving the processed image to a JPEG file.
- The following image processing commands are supported: -
    -
  • - Contrast
    - Keyboard: Ctrl - +/-
    - Mouse: Use processing panel
    - Increases or decreases the global contrast of the image. Technically, the steepness - of an S-curve is changed by this parameter.
    - This value can be stored in the parameter DB. -
  • -
  • - Brightness
    - Keyboard: Shift - +/-
    - Mouse: Use processing panel
    - Increases or decreases the global brightness of the image. Technically, a exponent of - a gamma curve is changed by this parameter.
    - This value can be stored in the parameter DB. -
  • -
  • - Saturation
    - Keyboard: none
    - Mouse: Use processing panel
    - Increases or decreases the color saturation of the image.
    - This value can be stored in the parameter DB. -
  • -
  • - Downsampling Sharpness
    - Keyboard: Alt - +/-
    - Mouse: Use processing panel. Note that this slider is only visible when the window/screen is large enough.
    - Increases or decreases sharpness of the image. If the default sharpening filter is used, sharpness - only applies to images that are downsampled but not for images displayed with 100% zoom. The - filter to be used can be set in the INI file.
    - This value can be stored in the parameter DB. -
  • -
  • - Unsharp Mask
    - Keyboard: none
    - Mouse: Use processing panel, 'Unsharp Mask' button.
    - Unsharp masking is an adaptive sharpening filter that creates a smoothed version of the original image (thus the name 'unsharp'). - The filter then adds the difference of the original image and the smoothed image to the original image:
    - Sharpened Image = Original Image + Amount * ( Original Image - Smoothed Image)
    - The 'Radius' parameter sets the radius of the smoothing kernel, the 'Amount' parameter sets the amount of sharpening and the - 'Threshold' value sets the miminal difference of original and smoothed image to apply the filter to a pixel.
    - Note that unsharp masking is previewed always in 100 % zoom.
    - This value cannnot be stored in the parameter DB. The image must be saved ('Save image' in context menu) to retain the processing. -
  • -
  • - Free Rotate
    - Keyboard: none
    - Mouse: Use navigation panel, 'Free Rotate' button.
    - In preview mode the image can be rotated with the mouse in real time, but with low quality. Grid lines help performing horizon correction. - Alternatively, horizon correction can be done by drawing a line along the horizon or along a vertical structure. The rotation angle will be calculated so - that this line is exactly horizontal, respecitively vertical. - Activate this mode with the right most tool button on the panel.
    - Press the 'Apply' button to apply the rotation to the original image with high quality (bicubic interpolation). By default the - rotated image is cropped automatically to avoid black borders. This behavior can be changed with the middle tool button on the rotation panel.
    - This value can be stored in the parameter DB. -
  • -
  • - Perspective Correction
    - Keyboard: none
    - Mouse: Use navigation panel, 'Perspective Correction' button.
    - When a picture of a building is taken from ground level, the vertical edges of the building are usually tilted because the camera is not aligned - horizontally when taking the picture. Perspective correction can be used to correct for this effect - at least partially.
    - In preview mode the image can be tilted with the mouse in real time, but with low quality. Grid lines help during the process.
    - Press the 'Apply' button to apply the correction to the original image with high quality (bicubic interpolation). By default the - corrected image is cropped automatically to avoid black borders. This behavior can be changed with the right tool button on the panel.
    - This value cannnot be stored in the parameter DB. The image must be saved ('Save image' in context menu) to retain the processing. -
  • -
  • - Change Size
    - Keyboard: none
    - Mouse: Use context menu, 'Transform image > Change size...' menu item.
    - This will resize the image to the specified new size using one of the selectable filters. The 'Box' filter is just - repeating pixels (upsampling), respectively deleting pixels (downsampling). The other filters are higher order reconstruction filters - that balance between no aliasing and improved sharpness.
    - This value cannnot be stored in the parameter DB. The image must be saved ('Save image' in context menu) to retain the processing. -
  • -
  • - Color balance
    - Mouse: Use processing panel
    - Changes the color balance of the image by changing the cyan-red, magenta-green and yellow-blue - color balance.
    - This value can be stored in the parameter DB. -
  • -
  • - Local brightness
    - Keyboard: F6
    - Mouse: Use processing panel
    - Toggle local brightness correction. Local brightness correction lightens shadows and darkens - highlights by applying a smoothed additive/subtractive mask to the image. It can be used to correct images with - hard contrasts and uncovers details in dark shadows or in overexposed areas.
    - This value can be stored in the parameter DB. -
  • -
  • - Color and contrast correction
    - Keyboard: F5
    - Mouse: Use processing panel
    - Toggle color and contrast correction. Color correction tries to reduce global color casts. Contrast - correction enhances contrast by moving the black point of the image (darkest area) towards black and the white point - (brightest area) towards white. The color and contrast correction algorithm can be configured in the INI file.
    - This value can be stored in the parameter DB. -
  • -
  • - Landscape picture enhancement mode
    - Keyboard: Ctrl-L
    - Mouse: Use navigation panel
    - Toggle landscape picture enhancement mode. The landscape picture mode is simply a special parameter set optimized - for landscapes pictures. It will brighten shadows, darken highlights and enhance contrast when needed. The parameter - set used in landscape mode can be configured in the INI file (setting "LandscapeModeParams").
    - This value can be stored in the parameter DB. -
  • -
- Most image processing can be done interactively with the mouse on the processing panel that appears when the mouse - cursor is moved to the bottom of the screen.
- Default settings for most of the image processing parameters can be given in the INI file.
- Image Processing Tips
-
    -
  • - Clicking on the number on the right of the sliders will temporarily reset the value to the default value. The value remains reset - until the mouse button is released. -
  • -
  • Double clicking above a slider will permanently reset its value to the default value.
  • -
- -

Keeping Parameters Between Images

- By default, all image processing parameters (including zoom and pan) are reset to the default values given in the INI file - when the next image is displayed. Using the F4 key, the behavior can be changed to keeping the parameters between - images. Pressing F4 again will toggle back to not-keeping the parameters.
- Note: While keeping parameters mode is active, the parameter DB cannot be used. Parameter DB values are ignored while - this mode is active. - -

Comparing Processed Image with Original Image

- When JPEGView displays a new image, it creates a second set of image processing parameters and initializes it with zero, - meaning that the original, unprocessed image will be displayed when this set is applied. - Ctrl-A toggles between the two sets - allowing to compare the processed image with the original image. - Note that it is possible to change both sets of parameters - however the second set will always be reset to zero when - a new image is displayed. - -

Saving and Renaming Images

-

Saving Images

- Processed images can be saved as JPEG, BMP, PNG or TIFF files using the 'Save image' (Ctrl-S) command. - The image is saved in its original size: Performed rotations are taken into account, zoom and pan are not. - To save an image in screen size, use the Ctrl-Shift-S keyboard shortcut.
- Note: The quality of the saved JPEG file can be set in the INI file.
- JPEGView will automatically create a parameter DB entry for the saved image having - all image processing disabled. - This prevents that JPEGView will double-apply automatic image improvements next time the image is displayed.
- Note: When saving images, JPEGView will prompt for a new file name. If you want that Ctrl-S just overrides the - original file without showing a dialog, use the INI file setting OverrideOriginalFileWithoutSaveDialog=true.
- Use this setting with care, be aware of the risk!
- EXIF information
- EXIF information is copied to the target JPEG image if it was present in the source JPEG image. An embedded thumbnail image - is updated - rotation and image processing is applied to the thumbnail image.
- -

Renaming Images

- On the processing panel, images can be renamed by clicking on the file name with the mouse. To - confirm the new name, press Enter or click outside of the edit control. To cancel renaming, press Esc. - Note that renaming images is not possible when a slide show file is loaded because this would invalidate the slide show file. - -

Batch Rename/Copy

- Using the context menu entry 'Batch rename/copy' a series of files from the current folder can be renamed and/or copied to - another folder: -
    -
  1. - First select the files to be renamed. Use the Ctrl and Shift keys to select multiple files and click the checkboxes. -
  2. -
  3. - Enter the target file name pattern. When not using a backslash in the pattern, the files are renamed in the current - folder, else they are copied to the target folder as given. Target folders not yet existing are created as needed.
    - Several placeholders are supported. See some examples below. A template pattern can be saved when needed. -
  4. -
  5. Before really renaming the files, always verify the new names by pressing the 'Preview' button.
  6. -
  7. - To rename the selected files, press the 'Rename' button. Renaming/copying can take a while, especially when - the source or target is a slow USB device. -
  8. -
- Examples

- %pictures%\Digicam\%2y-%m-%M\%f
- Source image: IMG00244.JPG, created June 12, 2007
- Target: 'My pictures'\Digicam\07-06-June\IMG00244.JPG
- The folder 'Digicam\07-06-June\' is created when needed.

- %pictures%\Digicam\%M %y\Trip to Barcelona %x.%e
- Source image: IMG00244.JPG, created June 12, 2007
- Target: 'My pictures'\Digicam\June 2007\Trip to Barcelona 1.JPG
- -

Copying Images to the Clipboard

- Copy screen
- Keyboard: Ctrl-C
- Copies the image (respectively the visible image section) as displayed on the screen to the clipboard. This is similar to the - PrtSc function, however text overlays and areas outside of the image are not included.
- Copy processed image in its original size
- Keyboard: Ctrl-X
- Copies the processed image in its original size to the clipboard. Rotation is taken into account, zoom and pan is not. - This command is useful when the corrected image shall be processed and saved in an external application. -

Pasting Images from the Clipboard

- Keyboard: Ctrl-V
- Paste image from clipboard and fit to screen.
- For pasted images, storing the parameters to the parameter DB is not possible - because they have no representation on the file system. However these images can be processed and saved. -

- Tip: Ctrl-C also enables to paste the current image file to a new location in the Windows Explorer using Ctrl-V. - -

Printing Images

- Keyboard: Ctrl-P
- Some aspects of printing can be changed in the INI file: -
    -
  • - Default margins
    - Setting: PrintMargin
    - Sets the default print margin (all sides) in centimeters. - Notice that you cannot set this value below the physical margins as specified by the printer driver. -
  • -
  • - Default width of printed image
    - Setting: PrintWidth
    - Sets the default print width in centimeters. Use negative numbers to set the 'Fit to paper' printing mode. -
  • -
  • - Units in print dialog
    - Setting: Units
    - Sets the units (cm, inches) used in the print dialog. By default the units as specified by the operating system are used. - Set to 'metric' or 'english' to force using centimeters, respectively inches. -
  • -
- Notice that JPEGView uses the default printer as specified in the Windows printer configuration but allows changing the printer for - the time JPEGView is running. To change the default printer permanently, use the Windows printer configuration. - -

Parameter Database

- The currently visible section, rotation and the image processing parameters can be bound to the image by storing them to the - parameter DB. If this image is displayed in JPEGView later on, the stored parameters will be loaded and applied automatically. -
    -
  • - Add image to the parameter DB
    - Keyboard: S
    - Mouse: Use processing panel -
  • -
  • - Remove image from the parameter DB
    - Keyboard: D
    - Mouse: Use processing panel -
  • -
  • - Backup parameter DB
    - Use this command in the context menu to backup the parameter DB to another folder or disk. -
  • -
  • - Restore parameter DB
    - Use this command in the context menu to restore the parameter DB from a backuped version. - Note that when restoring the parameter DB, the selected DB is merged with the current DB - so this - command can also be used to merge two parameter DBs. In case of conflicting entries in the merge, - the system asks which entries shall have priority. -
  • -
- Using the parameter DB has the advantage that the original image remains untouched. The parameter DB entry for an - image is found using a hash value of the image file what has the following advantages: -
    -
  • Moving the image to a different folder or renaming the image does not break the link
  • -
  • Two copies of the same image have the same parameter DB entry
  • -
- For JPEG images, the hash value is created on the compressed pixel data but not on the EXIF and comment blocks. Changing - the EXIF data or commenting the JPEG image will not invalidate the parameter DB entry.
- The following parameters are stored in the parameter DB: -
    -
  • - Visible section of image
    - The visible section is stored resolution independent - the same section will be displayed on a monitor with a higher - resolution (but the zoom factor will be different then). -
  • -
  • Rotation of image
  • -
  • Contrast value
  • -
  • Brightness value
  • -
  • Saturation value
  • -
  • Sharpness value (but no Unsharp Mask)
  • -
  • Color balance values
  • -
  • Local brighness correction on/off
  • -
  • Lighten shadows value
  • -
  • Darken highlights value
  • -
  • Deep shadows value
  • -
  • Color and contrast correction on/off
  • -
  • Color correction amount
  • -
  • Contrast correction amount
  • -
  • High quality resizing on/off
  • -
- The parameter database is located in 'Users\Username\AppData\roaming\JPEGView\ParamDB.db', - respectively in the EXE path when StoreToEXEPath=true is set in the INI file. - It uses 40 bytes per stored parameter set and is limitated to 100000 entries.
- Note: The parameter DB settings have precedence over any image processing settings in the INI file. - -

JPEGView.ini File

- The INI file of JPEGView allows configuring a lot of parameters of the application. A two level hierarchy of INI files is - used. If a setting is found in the user INI file, it has precedence over the same setting in the global INI file. -
    -
  • - User INI file
    - 'Users\Username\AppData\roaming\JPEGView\JPEGView.ini' (path name depends on Windows version) -
  • -
  • - Global INI file
    - 'Path of JPEGView.exe\JPEGView.ini' -
  • -
- The user INI file can be edited by using the command 'Edit user settings' in the context menu. - Note that changes to these files do not take effect until JPEGView is restarted.
- Some parameters can be written to the user INI file with the command 'Save parameters to INI' located in the - context menu. -
    -
  • Default contrast
  • -
  • Default brightness
  • -
  • Default saturation
  • -
  • Default sharpness
  • -
  • Default color balance
  • -
  • Local brighness correction on/off by default
  • -
  • Color and contrast correction on/off by default
  • -
  • Default sorting order
  • -
  • Default auto zoom mode
  • -
  • Navigation panel on/off
  • -
- The values set for the current image are written to the INI file when using this command. Be - aware that these values are then used as default values for all images!

- Note: No user INI file is used if the key StoreToEXEPath is set - to true in the global INI file. All saved setting are directly written to the global INI file in this case. -

INI File Settings

- The INI file settings are explained in detail in the INI file (Caution: The link is to the global INI) itself. - Only an overview on the available settings is given here: -
    -
  • Default image processing parameters: Contrast, brightness, sharpness, color balance
  • -
  • Default background color
  • -
  • Show file name by default
  • -
  • Show file info by default
  • -
  • Show navigation panel by default
  • -
  • Blending factor for navigation panel
  • -
  • Keep parameters by default
  • -
  • Force CPU type (if autodetection fails)
  • -
  • Force number of CPU cores to be used
  • -
  • Used filter kernel for downsampling images
  • -
  • Default sorting order
  • -
  • Default navigation mode
  • -
  • Default auto zoom mode
  • -
  • Quality (and file size) of saved JPEG images
  • -
  • Monitor to use (for multi monitor systems)
  • -
  • Color and contrast correction settings
  • -
  • Folders to explicitely include/exclude from auto color and contrast correction
  • -
  • Local brighness correction settings
  • -
  • Folders to explicitely include/exclude from local brightness correction
  • -
  • Parameters used in landscape enhancement mode
  • -
  • - Definition of user commands -
  • -

- - A note on the support for multiple CPU cores (CPUCoresUsed setting):
- By default, JPEGView uses all cores of the CPU for processing images. To force JPEGView to use - a given number of parallel processing threads, use the CPUCoresUsed setting. - Note that no more than 4 cores (and therefore 4 processing threads) are supported. - One additional thread is always created to read ahead the next image - this cannot be suppressed by the INI file. - -

Command Line Parameters

- - JPEGView supports the following command line parameters: -
    -
  • - filename
    - File name of the file to load or folder to show. If this is a file name, it must be a valid - image file name or a file name of a text file that contains a list of files to display, listing one file name per line. - If it is a folder name, JPEGView displays the image files in this folder.
    - The file name must be specified before all other parameters in the command line.
    - Note: No slide show is started automatically. Use the /slideshow parameter for that. -
  • -
  • - /slideshow [time in seconds]
    - Automatically starts a slide show after startup. Example:
    - jpegview.exe myfilelist.txt /slideshow 3
    - Starts a slideshow of the files listed in "myfilelist.txt" with an interval of 3 seconds between the images. -
  • -
  • - /effect [effect]
    - Sets the transition effect for full screen slideshows. The parameter has no effect if not used in fullscreen mode.
    - Possible effects are: None, Blend, SlideLR, SlideRL, SlideTB, SlideBT, RollLR, RollRL, RollTB, RollBT, ScrollLR, ScrollRL, ScrollTB, ScrollBT
    - Example:
    - jpegview.exe myfilelist.txt /slideshow 3 /effect blend /fullscreen -
  • -
  • - /transitiontime [time in milliseconds]
    - Sets the transition time for the effect in full screen slideshows. Example:
    - jpegview.exe myfilelist.txt /slideshow 3 /effect blend /transitiontime 250 /fullscreen -
  • -
  • - /monitor [monitor index]
    - Sets the monitor to display JPEGView, overriding the INI file setting. 0 stands for the primary monitor, - any number bigger than 0 for the secondary monitor with the corresponding index. Example:
    - jpegview.exe "d:\pictures\jan 2013\" /slideshow 3 /fullscreen /monitor 1 -
  • -
  • - /autoexit
    - Terminates JPEGView automatically after having finished the slide show if combined with the /slideshow parameter.
    - Setting this parameter also changes the behavior of the ESC key: JPEGView terminates immediately when pressing the ESC key, - also while playing a slideshow or an animated GIF.
    - Note that WrapAroundFolder must be set to false in the INI file - otherwise the slideshow does not finish when the - end of the folder is reached. -
    -
  • -
  • - /fullscreen
    - Starts JPEGView in full screen mode, ignoring the INI file setting that is currently active. -
    -
  • -
  • - /order [mode]
    - Forces file ordering mode 'mode', ignoring the INI file setting that is currently active.
    - [mode] can be M, C, N or Z, for the file ordering modes 'M: modification date', 'C: creation date', 'N: file name', 'Z: random'
    - Example: jpegview.exe c:\pictures /slideshow 2 /order z /fullscreen
    - Shows a slideshow of the files in the folder "c:\pictures" using random ordering and an interval of 2 seconds, forcing full screen mode for the slide show. -
  • -
  • - /ini [ini file title]
    - Uses the INI file as specified. The file extension can be omitted, ".ini" is added when it is missing. - The file name must neither contain an absolute nor a relative path, nor space characters.
    - When the specified INI file contains the key StoreToEXEPath=true, it must be located in the EXE path and it must exist. - Otherwise, the INI file will be created in the user INI path (%appdata%/roaming/JPEGView/) when it does not exist and when it is used - the first time.
    - Example: jpegview.exe /ini AlternativeConfig
    - Uses an INI file named "AlternativeConfig.ini" in %appdata%/roaming/JPEGView/ -
  • -
- -

Keymap - Changing Keyboard and Mouse Shortcuts

- The mapping of keys to commands can be changed by editing the keymap file.
- Within the keymap file it is also possible to define additional keyboard shortcuts for command that are - not mapped by default. The keymap file also allows defining a command that gets executed when pressing one - of the mouse buttons. Be aware that by doing so, the default mouse button functionality will be disabled.
- When editing this file, make sure you follow the instructions mentioned in the keymap file itself. - -

Adding Support for Viewing Camera RAW Files to JPEGView

- JPEGView has built in support to view the JPEG images embedded in many camera RAW files. These embedded JPEGs often - have a smaller resolution than the RAW image itself but can be decoded and displayed very quickly.
- Support for viewing the full size camera RAW images is provided indirectly by using the Microsoft Camera Codec pack.
- This codec pack is only available for Windows Vista SP2 and Windows 7/8, but not for Windows XP!
- To add support for viewing full size camera RAW files: -
    -
  • - Download and install the Microsoft Camera Codec pack.
    - Check if your camera model is supported by the codec pack. -
  • -
  • - Add the extension(s) of the camera RAW files to be read by JPEGView to the user INI file, setting 'FilesProcessedByWIC'.
    - Context menu: Settings/Admin > Edit user settings...
    - Example for adding the extension "cr2", used by Canon cameras:
    - - FilesProcessedByWIC=*.wdp;*.mdp;*.hdp;*.cr2 - -
  • -
  • - Save the INI file and restart JPEGView. -
  • -
- Be warned that decoding the full size RAW images is much, much slower than just decoding the embedded JPEG! - -

Calling External Programs or Batch Files

- In the INI file, user commands can be defined. User command can call any external program or batch file and can be integrated into JPEGView using: -
    -
  • - Binding to a keyboard shortcut. -
  • -
  • - Binding to a menu item in the 'User Commands' submenu of the JPEGView context menu. -
  • -
- Refer to the INI file itself for a full description of the format and the features.
- JPEGView itself uses this mechanism to implement the following commands: -
    -
  • - Shift-Del : Delete the current image file permanently - calls the command processor (cmd.exe) to - delete the file. -
  • -
- Other commands can be added as needed, e.g. moving an image to a specific folder. - -

Opening Images with External Applications

- JPEGView supports opening the currently displayed image in an external application (e.g. GIMP) if the application supports passing a file - name on the command line (as most applications do).
- To define external applications, use the 'Settings/Admin' - 'Manage 'Open with' menu' dialog. - The defined applications are visible in the 'Open with' submenu of the JPEGView context menu. Binding an application to a keyboard shortcut is also possible. - -

Troubleshooting

- JPEGView crashes when displaying an image
- Maybe the CPU detection failed. Try to set the CPU type manually in the INI File (key 'CPUType'). Set this - key to 'Generic' - this is slower but should run on all CPUs.

- - The file type .JPG can not be assigned to JPEGView using the method described in the - installation section. JPEGView never appears in the list of programs for JPG files even - when it is added with 'Browse'. -
- Maybe JPEGView was moved to another folder while the file type was already registered to it. In this case Windows - loses the connection and this cannot be fixed with the Windows Explorer. Start regedit.exe and check the path in - 'HKEY_CLASSES_ROOT\Applications\JPEGView.exe\shell\open\command'. Correct the path if it points to an invalid location.

- - The language of the user interface is in English instead of 'Portuguese/French/whatever'
- Normally JPEGView selects the UI language automatically to match the operating system language. - This auto detection may has failed. To force a specific language, set the
- Language=language_code
- setting in the INI file to the desired language code. The supported codes are listed in the INI file.

- - The colors of images do not look the same compared to other viewers and to the Internet browser
- By default, JPEGView is not interpreting embedded color profiles in JPEG and TIFF images. You can force using these - color profiles by setting the following key to true in the INI file:
- UseEmbeddedColorProfiles=true
- Be aware the this slows down loading of JPEG images considerably (also when no color profile is embedded) - because it forces JPEGView to read the JPEG image with GDI+ instead of using the optimized TJPEG library. -

- - The image set as desktop wallpaper does not span all screens.
- In Windows 7 / 8 and later a desktop wallpaper image can span multiple screens. To create such a wallpaper, switch to fullscreen - mode (F11 key) and expand JPEGView to span all screens (F12 key). Then zoom in the image so that no black borders are visible on - any side of the screen and pan the image as desired. Finally select 'Set as desktop wallpaper' > 'Use processed image as displayed'. - -

License & Copyright

- Copyright © 2006-2017 David Kleiner

- This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, version 2. - The full text of the GPL can be found here.
- JPEGView is hosted by SourceForge. You can check for a new version, - get the source code or report a bug using the following URL:
- https://sourceforge.net/projects/jpegview/
- -
- JPEGView contains the following binary libraries:
- - Turbo JPEG, Copyright (C)2009-2011 D. R. Commander. All Rights Reserved.
- Turbo JPEG is based on libjpeg, © 1994-2010, Independent JPEG Group.
- - WebP codec provided by Google https://developers.google.com/speed/webp, © Google Inc.
-
- JPEGView includes or uses the following source code libraries:
- - Windows Template Library 8.1, Copyright © 2006 Microsoft Corporation
- - dcraw, © 1997-2011 by Dave Coffin
-
- Developed and compiled with Visual Studio 2010/2013 Professional -

- Thanks to:
-
    -
  • - Armel for testing, bug reporting and suggestions for improvements since the very first versions -
  • -
  • - Michi, Patrik and Michael for additional testing and ideas -
  • -
  • - Anonymous for integration of dcraw, for viewing JPEGs embedded in camera RAW -
  • -
  • - Franco Bianconi, Daniel Sánchez Vargas and Enrique Pineiro for the Spanish translation -
  • -
  • - winPenPack Team and Massimo Pietrolonardo for the Italian translation -
  • -
  • - Adil Tiadi and Fabrice for the French translation -
  • -
  • - Anonymous for the Korean translation -
  • -
  • - Silviu Laurentiu for the Romanian translation -
  • -
  • - Dmitry Yerokhin for the Russian translation -
  • -
  • - Åke Engelbrektson for the Swedish translation -
  • -
  • - Carpegieri Torezan for the Portuguese (Brasilian) translation -
  • -
  • - Milos Koutny for the Czech translation -
  • -
  • - Paris Setos for the Greek translation -
  • -
  • - Sérgio Marques for the Portuguese translation -
  • -
  • - Xabier Aramendi for the Basque translation -
  • -
  • - Yia Guo and 风逸兰 for the Chinese translations -
  • -
  • - Uladzimir Panada for the Belarusan translation -
  • -
  • - Володимир for the Ukrainian translation -
  • -
  • - Hiroyuki Matenokoji for the Japanese translation -
  • -
  • - Damian for the Polish translation -
  • -
-
-
- - - - \ No newline at end of file From 5985a389f4ab933c007347401f2e13f43885cbd8 Mon Sep 17 00:00:00 2001 From: Vivor Loong Date: Tue, 23 Dec 2025 12:08:49 +0800 Subject: [PATCH 3/3] Cleanup 1.x: Removed remaining root files --- README.md | 182 -------------------- RELEASE_NOTES.md | 153 ---------------- custom-triplet/x64-windows-static-opt.cmake | 21 --- 3 files changed, 356 deletions(-) delete mode 100644 README.md delete mode 100644 RELEASE_NOTES.md delete mode 100644 custom-triplet/x64-windows-static-opt.cmake diff --git a/README.md b/README.md deleted file mode 100644 index bf11d70c..00000000 --- a/README.md +++ /dev/null @@ -1,182 +0,0 @@ -
- -QuickView Banner - -# âš¡ QuickView 2.0 - -### The Rebirth: Built for Speed, Engineered for Geeks. - -

- Direct2D Rendering • - AVX2/SIMD Optimized • - Dynamic Island UI -

- -

- - License - - - Platform - - - Release - -

- -

- 📥 Download Latest Release • - Report Bug • - Request Feature -

- -
- ---- - -## 💥 2.0 Preview: The Rebirth - -**QuickView 2.0 represents a complete break from the past.** - -This is not a simple iteration. It is a ground-up rewrite based on **Modern C++ (C++23)** and **Direct2D**. We have discarded the legacy GDI rendering baggage and integrated the industry's most cutting-edge decoding technologies (Google Wuffs, TurboJPEG v3, dav1d) with one goal in mind: **To build the fastest, most hardcore image viewer on the Windows platform.** - ---- - -## 🚀 Key Features - -### �� Extreme Performance Engine - -> *"Speed is a feature."* We squeeze every ounce of performance out of your hardware. - -
- Task Manager Proof -
- Figure: 18.6MB RAM usage while rendering high-res content with Real-time Histogram. -
- -
- -* **Direct2D Pipeline:** Fully GPU-accelerated. Panning, zooming, and rendering on 4K high-refresh displays run at a silky smooth **60fps+**. -* **Dual-Lane Scheduling:** Unique "Fast/Slow" thread pools ensure that loading massive RAW files in the background **never** blocks UI responsiveness. -* **Instant Preview:** For RAW (ARW, CR2, DNG...), HEIC, and PSD, we extract embedded preview streams for **zero-latency** opening of 200MB+ files. - -### �� Dynamic Island Toolbar (New!) - -*A UI that breathes with you.* - -* **Immersive Interaction:** The toolbar floats elegantly at the bottom, mimicking the "Dynamic Island" aesthetic. -* **Hover-Reactive:** It remains invisible to maximize viewing area and seamlessly expands when your mouse approaches the bottom edge. -* **Geek Tools:** Instant access to Format Repair, RAW Toggle, and Exif data without cluttering the screen. - -### 🖼� HUD Photo Wall - -*Browse visually, not just sequentially.* - -
- HUD Photo Wall -
- Figure: HUD Photo Wall. -
- -* **"T" to Toggle:** Press `T` to summon the Photo Wall overlay in a Heads-Up Display style. -* **Virtualization:** Effortlessly handles folders with **10,000+ images**. We only render what is currently visible on screen. -* **Smart Caching:** Dual-layer RAM (L1) + VRAM (L2) architecture with strict memory capping. - -### 📊 Geek Visualization - -*Don't just view the image; understand the data.* - - -* **Real-time RGB Histogram:** Translucent waveform overlay for exposure analysis. -* **Reverse Q-Factor:** Algorithmically estimates the original JPEG quality (e.g., `Q~98`), seeing through "100% Quality" saves. -* **Chroma Subsampling:** Instantly identify **4:4:4** (Lossless/Design) vs **4:2:0** (Compressed). - ---- - -## ⚙� The Engine Room: Format Support - -QuickView 2.0 doesn't just "open" images; it uses the fastest, most secure libraries available for each format. - -| Category | Format | Backend Engine | Why it rocks (Architecture) | -| :--- | :--- | :--- | :--- | -| **Standard** | **JPEG** | **libjpeg-turbo v3** | AVX2 SIMD acceleration. The king of decompression speed. | -| | **PNG** | **Google Wuffs** | Memory-safe, outperforms libpng, handles massive dimensions. | -| | **GIF** | **Google Wuffs** | Extremely low CPU usage for playback (Better than WIC). | -| | **WebP** | **libwebp** | Google's official library. Supports Lossless, Animation, & Alpha. | -| | **BMP / TGA**| **Google Wuffs** | Replaces WIC for robust handling of malformed files. | -| **Modern** | **AVIF** | **libavif + dav1d** | Assembly-level AV1 decoding. Opens AVIFs instantly. | -| | **JXL** | **libjxl + highway** | Next-gen JPEG XL standard with multi-threaded decoding. | -| **Pro / RAW**| **RAW** | **LibRaw** | Prioritizes "Instant Preview" extraction for speed. | -| | **EXR** | **TinyEXR** | Lightweight, industrial-grade OpenEXR support. | -| | **HDR** | **stb_image** | Radiance HDR format support. | -| | **PSD** | **stb_image** | Reads composite preview layer for instant viewing. | -| **Vector** | **SVG** | **NanoSVG** | Rasterizes vector to bitmap for infinite scaling. | -| **System** | **HEIC** | **Windows WIC** | Hardware accelerated (Requires HEIF Extension). | - ---- - -## ⌨� Shortcuts - -Master these shortcuts to navigate QuickView at the speed of thought: - -### �� Viewing & Navigation -| Key | Action | Description | -| :--- | :--- | :--- | -| **`T`** | **Photo Wall** | Toggle the HUD Gallery overlay. | -| **`1`** / **`Z`** | **100% View** | Pixel-perfect inspection (Actual Size). | -| **`0`** / **`F`** | **Fit to Screen** | Fit the entire image to the screen. | -| **`Enter`** / **`F11`** | **Fullscreen** | Toggle immersive fullscreen mode. | -| **`Esc`** | **Exit** | Exit fullscreen or close the application. | -| **`Tab`** | **Lite Info** | Cycle through basic OSD overlays. | -| **`I`** | **Full Info** | Toggle the detailed EXIF/Histogram panel. | -| **`Ctrl + T`** | **Always on Top** | Pin the window to the foreground. | - -### 🔄 Transform & Edit -| Key | Action | Description | -| :--- | :--- | :--- | -| **`R`** | **Rotate CW** | Rotate 90° Clockwise. | -| **`Shift + R`** | **Rotate CCW** | Rotate 90° Counter-Clockwise. | -| **`H`** | **Flip Horizontal** | Mirror the image horizontally. | -| **`V`** | **Flip Vertical** | Mirror the image vertically. | -| **`E`** | **Edit** | Open image in default external editor. | - -### � File Operations -| Key | Action | Description | -| :--- | :--- | :--- | -| **`O`** / **`Ctrl + O`** | **Open File** | Open file dialog. | -| **`F2`** | **Rename** | Rename the current file on disk. | -| **`Del`** | **Delete** | Move current file to Recycle Bin. | -| **`Ctrl + C`** | **Copy Image** | Copy image data to Clipboard. | -| **`Ctrl + Alt + C`** | **Copy Path** | Copy full file path text. | -| **`Ctrl + P`** | **Print** | Open system print dialog. | - ---- - -## 📥 Download & Install - -**QuickView is Portable.** No installers, no registry bloat. - -1. Go to the [**Releases Page**](https://github.com/justnullname/QuickView/releases). -2. Download the latest `QuickView_2.0_Preview_x64.zip`. -3. Unzip and run `QuickView.exe`. - ---- - -## 🗺� Roadmap - -We are moving fast. Here is our current progress: - -- [x] **Core Engine 2.0** (D2D, Wuffs, TurboJPEG) -- [x] **HUD Photo Wall** (Virtualization, Smart Cache) -- [x] **Geek Panel** (Histogram, Q-Estimator) -- [x] **Dynamic Island Toolbar** (Hover-reactive UI) -- [x] **Full Format Support** (SVG, EXR, PSD, JXL, AVIF) -- [ ] **Visual Settings UI** (Replacing manual .ini editing) -- [ ] **Gif/WebP Frame Inspector** (Frame-by-frame analysis) - ---- - -## ⚖� License & Credits - -This project is licensed under the **GPL-2.0 License**. -QuickView stands on the shoulders of giants. We salute **David Kleiner** (original JPEGView) and the open-source community behind **LibRaw, LibJpeg-Turbo, Google Wuffs, dav1d, and TinyEXR**. \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md deleted file mode 100644 index 59a39a3b..00000000 --- a/RELEASE_NOTES.md +++ /dev/null @@ -1,153 +0,0 @@ -# QuickView v1.3.0 Release Notes - -**Context Menu Modernization & OSD Fixes** -* **Cleaner Context Menu**: Reduced clutter by moving "Print", "Batch Copy", "Wallpaper", and "Transform" into a new **"Tools"** submenu. -* **Enhanced OSD**: Fixed a bug where the zoom percentage OSD would lag when using double-click to zoom. - -# QuickView v1.2.1 Release Notes - -**Visual Feedback & Polish** -This update focuses on improving user feedback and code quality. We've introduced a unified On-Screen Display (OSD) system that provides clear visual confirmation for "Always on Top" and other states. - -## 🚀 New Features & Improvements - -### 🖥� Always on Top OSD -* **Visual Confirmation**: Toggling "Always on Top" (`Shift + F12`) now displays a clear "ON" or "OFF" message in the center of the screen. -* **Modern Design**: Using a sleek, semi-transparent dark background with white Segoe UI text. - -### ⚡ Unified OSD System (Technical) -* **Refactored Core**: All OSD messages (Zoom, System Status) now run on a unified "OSD Manager". -* **Zero Latency**: Zoom percentage display is now instant (bypassing the previous 200ms optimization delay for purely visual feedback). -* **Consistent Look**: Zoom and System messages share the same visual style. - ---- - -# QuickView v1.2.0 Release Notes - -**The Smart Update** -QuickView v1.2.0 brings intelligence to your viewing experience with background updates, smart zooming, and instant gallery browsing. - -## 🚀 New Features - -### 🔄 Seamless Auto-Update -* **Silent Background Download**: Updates download while you utilize the app. No waiting bars. -* **One-Click Install**: When ready, a simple dialog lets you update instantly. -* **Smart Cleanup**: Temporary files are automatically removed. - -### � Smart Double-Click Zoom -* **One-Two Punch**: Double-click to inspect details at 100% pixel-perfect zoom. Double-click again to fit to window. -* **Zero Latency**: Optimized for instant response. - -### 🖼� Instant Gallery Mode -* **Toggle with 'T'**: Press 'T' to instantly show/hide the thumbnail bar. -* **Overlay Design**: Modern overlay that doesn't resize your window content unnecessarily. - -## ✨ Improvements -* **Markdown Support**: Release notes in the update dialog now look cleaner with automatic Markdown formatting. -* **Localization**: Full support for interface translation. -* **Bug Fixes**: Fixed issues with ZIP download handling and version string display (ANSI/Unicode). - ---- - -# QuickView v1.1.6 Release Notes - -**Narrow Border Mode Restored** -This update restores the "Narrow Border Mode" feature, allowing a configurable border to be drawn in borderless window mode for better visibility. - -## 🚀 New Features - -* **Narrow Border Mode**: Restored the ability to draw a colored border around the window when in borderless mode. - * **Configuration**: Add the following to your `QuickView.ini` under `[QuickView]`: - ```ini - ; Width of the border in pixels (0 = disabled) - NarrowBorderWidth=2 - ; Color of the border (R G B) - NarrowBorderColor=128 128 128 - ``` - ---- - -# QuickView v1.1.5 Release Notes - -**Window Resizing Fix** -This update resolves an issue where the window would not correctly resize to accommodate the thumbnail bar, causing the image to be cropped. - -## � Bug Fixes - -# QuickView v1.1.4 Release Notes - -**Initialization & Cropping Fix** -This update resolves issues where images could appear cropped on startup or when the thumbnail bar was toggled. - -## � Bug Fixes - -* **Startup Cropping**: Fixed a bug where the "Fit to Screen" state was uninitialized on startup, leading to random cropping behavior. -* **Panning Reset**: Fixed an issue where the image would remain shifted (panned) when resizing to fit the thumbnail bar, causing the top or bottom to be cut off. Now, the image is correctly centered whenever it auto-resizes. - ---- - -# QuickView v1.1.3 Release Notes - -**Zoom Fix Release** -This update fixes a regression in v1.1.2 that prevented manual zooming. - -## � Bug Fixes - -* **Zoom Regression**: Fixed an issue where manual zooming was disabled (locked to "Fit to Screen") when the thumbnail bar fix was applied. Now, "Fit to Screen" correctly resizes for the thumbnail bar, but manual zooming works freely as expected. - ---- - -# QuickView v1.1.2 Release Notes - -**Refined Hotfix Release** -This update refines the fix for the thumbnail bar overlap issue, ensuring images are properly rescaled to fit the available space. - -## � Bug Fixes - -* **Thumbnail Bar Overlap (Refined)**: In v1.1.1, the image was shifted but not resized, causing the top to be cut off. v1.1.2 correctly recalculates the "Fit to Screen" zoom level when the thumbnail bar appears, ensuring the entire image is visible. - ---- - -# QuickView v1.1.1 Release Notes - -**Hotfix Release** -This update addresses a visual issue introduced in v1.1.0 where the thumbnail bar would overlap the main image. - -## � Bug Fixes - -* **Thumbnail Bar Overlap**: Fixed a bug where the main image was not correctly resized/positioned when the thumbnail bar was visible, causing the bottom of the image to be covered. The image now correctly fits within the available space above the thumbnail bar. - ---- - -# QuickView v1.1.0 Release Notes - -**This is a major release marking a significant evolution of QuickView.** -Building upon the solid foundation of JPEGView, this version introduces original features that transform the user experience, making QuickView not just a viewer, but a modern, borderless image consumption tool. - -## 🚀 Major New Features (Original) - -### 1. Thumbnail Gallery Navigation -* **What it is**: A completely new way to browse your folder. Instead of just next/prev, see a filmstrip of your images at the bottom. -* **New in v1.1.0**: Added dedicated left/right navigation arrows for independent scrolling, allowing you to peek at upcoming images without changing the main view. -* **Design**: Modern, transparent "chevron" arrows that blend seamlessly with the UI. - -### 2. True Borderless Experience -* **What it is**: We've removed the clutter. No title bars, no ugly borders. Just your image, edge-to-edge. -* **Chrome-style Floating Controls**: To maintain usability without borders, we've added a sleek, auto-hiding button bar (Minimize, Maximize, Close) that appears only when you need it (hover at the top). - -### 3. Startup Optimization -* **Optimization**: Eliminated the "white screen flash" on startup. QuickView now respects your dark theme from the very first millisecond. - -## � Critical Bug Fixes - -* **Zoom Text**: Fixed a glitch where zoom percentage text would overlap with the thumbnail bar. -* **Fullscreen Interaction**: Resolved issues where dragging the window was possible in fullscreen mode. -* **UI Stability**: Fixed various button visibility and interaction bugs. - -## 📦 Installation - -1. Download `QuickView_v1.1.0.zip`. -2. Extract the contents to a folder of your choice. -3. Run `QuickView.exe`. - -*Note: This is a portable release. All settings are stored in `QuickView.ini`.* diff --git a/custom-triplet/x64-windows-static-opt.cmake b/custom-triplet/x64-windows-static-opt.cmake deleted file mode 100644 index 51fb7a4a..00000000 --- a/custom-triplet/x64-windows-static-opt.cmake +++ /dev/null @@ -1,21 +0,0 @@ -set(VCPKG_TARGET_ARCHITECTURE x64) -set(VCPKG_CRT_LINKAGE static) -set(VCPKG_LIBRARY_LINKAGE static) - -# Common flags -# /Gw = Optimize global data -set(COMMON_FLAGS "/DWIN32 /D_WINDOWS /W3 /utf-8 /MP /Gw") - -set(VCPKG_C_FLAGS "${COMMON_FLAGS}") -set(VCPKG_CXX_FLAGS "${COMMON_FLAGS}") - -# Release: Maximize Speed (/O2), Intrinsic (/Oi), Inline (/Ob2), LTO (/GL) -set(VCPKG_C_FLAGS_RELEASE "/O2 /Oi /Ob2 /GL") -set(VCPKG_CXX_FLAGS_RELEASE "/O2 /Oi /Ob2 /GL") -set(VCPKG_LINKER_FLAGS_RELEASE "/LTCG") - -# Debug: Default (no forced optimizations, allows /RTC1) - -# zlib-ng: Use ZLIB_COMPAT mode to replace zlib with faster zlib-ng -# This makes zlib-ng output zlib-compatible library names and APIs -set(ZLIB_COMPAT ON)