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.ico b/QuickView.ico
deleted file mode 100644
index c3a5137b..00000000
Binary files a/QuickView.ico and /dev/null differ
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