diff --git a/packages/media/cpp/dist/pm-image.exe b/packages/media/cpp/dist/pm-image.exe index 12ae5e73..57881d68 100644 Binary files a/packages/media/cpp/dist/pm-image.exe and b/packages/media/cpp/dist/pm-image.exe differ diff --git a/packages/media/cpp/dist/pm-image.pdb b/packages/media/cpp/dist/pm-image.pdb index 96a3e612..c9070477 100644 Binary files a/packages/media/cpp/dist/pm-image.pdb and b/packages/media/cpp/dist/pm-image.pdb differ diff --git a/packages/media/cpp/src/win/ui_next/Mainfrm.cpp b/packages/media/cpp/src/win/ui_next/Mainfrm.cpp index 2bf70685..1a974579 100644 --- a/packages/media/cpp/src/win/ui_next/Mainfrm.cpp +++ b/packages/media/cpp/src/win/ui_next/Mainfrm.cpp @@ -2,6 +2,7 @@ #include "Mainfrm.h" #include "Resource.h" #include "core/resize.hpp" +#include "core/transform.hpp" #include #include @@ -36,6 +37,7 @@ STDMETHODIMP CMainFrame::Execute(UINT32 cmdID, UI_EXECUTIONVERB verb, case IDC_CMD_ADD_FOLDER: OnAddFolder(); break; case IDC_CMD_CLEAR: OnClearQueue(); break; case IDC_CMD_RESIZE: OnResize(); break; + case IDC_CMD_TRANSFORM: OnTransform(); break; case IDC_CMD_ABOUT: OnHelp(); break; case IDC_CMD_EXIT: OnExit(); break; case IDC_RIBBONHELP: OnHelp(); break; @@ -256,6 +258,201 @@ void CMainFrame::OnResize() GetStatusBar().SetPartText(0, L"Resizing\u2026"); } +std::vector CMainFrame::GetSelectedQueueItems() +{ + std::vector sel; + if (!m_pDockQueue) return sel; + auto& lv = m_pDockQueue->GetQueueContainer().GetListView(); + int idx = -1; + while ((idx = lv.GetNextItem(idx, LVNI_SELECTED)) != -1) + sel.push_back(idx); + if (sel.empty()) { + int total = lv.QueueCount(); + for (int i = 0; i < total; ++i) sel.push_back(i); + } + return sel; +} + +static INT_PTR CALLBACK PromptDlgProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + switch (msg) { + case WM_INITDIALOG: { + ::SetWindowLongPtr(hwnd, GWLP_USERDATA, lp); + auto* prompt = reinterpret_cast(lp); + if (!prompt->empty()) + ::SetDlgItemTextW(hwnd, IDC_EDIT_PROMPT, prompt->c_str()); + + RECT rc; + ::GetClientRect(hwnd, &rc); + int w = rc.right - rc.left, h = rc.bottom - rc.top; + int margin = 10; + + HWND hLabel = ::GetDlgItem(hwnd, IDC_STATIC_PROMPT_LABEL); + ::SetWindowPos(hLabel, nullptr, margin, margin, w - 2*margin, 20, SWP_NOZORDER); + + HWND hEdit = ::GetDlgItem(hwnd, IDC_EDIT_PROMPT); + ::SetWindowPos(hEdit, nullptr, margin, 35, w - 2*margin, h - 80, SWP_NOZORDER); + + HWND hOk = ::GetDlgItem(hwnd, IDOK); + HWND hCancel = ::GetDlgItem(hwnd, IDCANCEL); + int btnW = 80, btnH = 28; + ::SetWindowPos(hOk, nullptr, w - 2*(btnW + margin), h - btnH - margin, btnW, btnH, SWP_NOZORDER); + ::SetWindowPos(hCancel, nullptr, w - btnW - margin, h - btnH - margin, btnW, btnH, SWP_NOZORDER); + + HFONT hFont = static_cast(::GetStockObject(DEFAULT_GUI_FONT)); + ::SendMessage(hLabel, WM_SETFONT, (WPARAM)hFont, TRUE); + ::SendMessage(hEdit, WM_SETFONT, (WPARAM)hFont, TRUE); + ::SendMessage(hOk, WM_SETFONT, (WPARAM)hFont, TRUE); + ::SendMessage(hCancel, WM_SETFONT, (WPARAM)hFont, TRUE); + + ::SetFocus(hEdit); + return FALSE; + } + case WM_COMMAND: + if (LOWORD(wp) == IDOK) { + auto* prompt = reinterpret_cast(::GetWindowLongPtr(hwnd, GWLP_USERDATA)); + wchar_t buf[4096]{}; + ::GetDlgItemTextW(hwnd, IDC_EDIT_PROMPT, buf, 4095); + *prompt = buf; + ::EndDialog(hwnd, IDOK); + return TRUE; + } + if (LOWORD(wp) == IDCANCEL) { + ::EndDialog(hwnd, IDCANCEL); + return TRUE; + } + break; + case WM_CLOSE: + ::EndDialog(hwnd, IDCANCEL); + return TRUE; + } + return FALSE; +} + +static HWND CreatePromptDialog(HWND parent, std::wstring& prompt) +{ + // Build a dialog template in memory + alignas(DWORD) BYTE buf[2048]{}; + DLGTEMPLATE* dlg = reinterpret_cast(buf); + dlg->style = DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE; + dlg->cdit = 4; // label, edit, OK, Cancel + dlg->cx = 280; dlg->cy = 160; + + WORD* p = reinterpret_cast(dlg + 1); + *p++ = 0; // menu + *p++ = 0; // class + // title "AI Transform - Enter Prompt" + const wchar_t title[] = L"AI Transform - Enter Prompt"; + memcpy(p, title, sizeof(title)); + p += (sizeof(title) / sizeof(WORD)); + + // Align to DWORD + if (reinterpret_cast(p) % 4) p++; + + auto addItem = [&](DWORD style, short x, short y, short cx, short cy, WORD id, const wchar_t* cls, const wchar_t* text) { + if (reinterpret_cast(p) % 4) p++; + DLGITEMTEMPLATE* item = reinterpret_cast(p); + item->style = style | WS_CHILD | WS_VISIBLE; + item->x = x; item->y = y; item->cx = cx; item->cy = cy; + item->id = id; + p = reinterpret_cast(item + 1); + // class + size_t clen = wcslen(cls) + 1; + memcpy(p, cls, clen * 2); p += clen; + // text + size_t tlen = wcslen(text) + 1; + memcpy(p, text, tlen * 2); p += tlen; + *p++ = 0; // extra + }; + + // Label + addItem(SS_LEFT, 6, 4, 268, 10, IDC_STATIC_PROMPT_LABEL, + L"Static", L"Describe the transformation:"); + // Multiline edit + addItem(ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_BORDER | WS_VSCROLL | WS_TABSTOP, + 6, 18, 268, 110, IDC_EDIT_PROMPT, L"Edit", L""); + // OK button + addItem(BS_DEFPUSHBUTTON | WS_TABSTOP, 150, 134, 56, 18, IDOK, L"Button", L"Transform"); + // Cancel button + addItem(WS_TABSTOP, 212, 134, 56, 18, IDCANCEL, L"Button", L"Cancel"); + + INT_PTR result = ::DialogBoxIndirectParam( + ::GetModuleHandle(nullptr), + dlg, parent, PromptDlgProc, + reinterpret_cast(&prompt)); + + return (result == IDOK) ? parent : nullptr; +} + +void CMainFrame::OnTransform() +{ + if (m_processing) { + ::MessageBox(GetHwnd(), L"Already processing.", L"pm-image", MB_ICONWARNING); + return; + } + if (!m_pDockQueue) return; + + auto indices = GetSelectedQueueItems(); + if (indices.empty()) { + ::MessageBox(GetHwnd(), L"Queue is empty \u2014 drop files first.", L"pm-image", MB_ICONINFORMATION); + return; + } + + std::wstring prompt = m_lastPrompt; + if (!CreatePromptDialog(GetHwnd(), prompt)) + return; + if (prompt.empty()) { + ::MessageBox(GetHwnd(), L"Prompt cannot be empty.", L"pm-image", MB_ICONWARNING); + return; + } + m_lastPrompt = prompt; + + std::string promptUtf8 = wide_to_utf8(prompt); + + // Resolve API key + std::string api_key; + const char* env_key = std::getenv("IMAGE_TRANSFORM_GOOGLE_API_KEY"); + if (env_key && env_key[0] != '\0') api_key = env_key; + + if (api_key.empty()) { + ::MessageBox(GetHwnd(), + L"API key not found.\nSet IMAGE_TRANSFORM_GOOGLE_API_KEY in your .env file.", + L"pm-image", MB_ICONERROR); + return; + } + + auto& lv = m_pDockQueue->GetQueueContainer().GetListView(); + std::vector> items; + for (int idx : indices) { + CString p = lv.GetItemPath(idx); + items.emplace_back(idx, wide_to_utf8(std::wstring(p.c_str()))); + } + + m_processing = true; + HWND hwnd = GetHwnd(); + + m_worker = std::thread([items = std::move(items), promptUtf8, api_key, hwnd]() { + int ok = 0, fail = 0; + for (auto& [idx, input] : items) { + ::PostMessage(hwnd, UWM_TRANSFORM_PROGRESS, (WPARAM)idx, 1); + + media::TransformOptions topts; + topts.prompt = promptUtf8; + topts.api_key = api_key; + + std::string output = media::default_transform_output(input, promptUtf8); + auto result = media::transform_image(input, output, topts, nullptr); + + ::PostMessage(hwnd, UWM_TRANSFORM_PROGRESS, (WPARAM)idx, result.ok ? 2 : 3); + if (result.ok) ++ok; else ++fail; + } + ::PostMessage(hwnd, UWM_TRANSFORM_DONE, (WPARAM)ok, (LPARAM)fail); + }); + m_worker.detach(); + + GetStatusBar().SetPartText(0, L"Transforming\u2026"); +} + void CMainFrame::OnExit() { Close(); @@ -298,13 +495,43 @@ LRESULT CMainFrame::OnQueueDone(WPARAM wparam, LPARAM lparam) return 0; } +LRESULT CMainFrame::OnTransformProgress(WPARAM wparam, LPARAM lparam) +{ + if (!m_pDockQueue) return 0; + auto& lv = m_pDockQueue->GetQueueContainer().GetListView(); + int idx = static_cast(wparam); + int state = static_cast(lparam); + if (state == 1) + lv.SetItemStatus(idx, L"Transforming\u2026"); + else if (state == 2) + lv.SetItemStatus(idx, L"\u2713 Transformed"); + else + lv.SetItemStatus(idx, L"\u2717 AI Error"); + return 0; +} + +LRESULT CMainFrame::OnTransformDone(WPARAM wparam, LPARAM lparam) +{ + m_processing = false; + int ok = static_cast(wparam); + int fail = static_cast(lparam); + CString s; + s.Format(L"Transform done: %d succeeded, %d failed", ok, fail); + GetStatusBar().SetPartText(0, s); + if (fail > 0) + ::MessageBox(GetHwnd(), s, L"pm-image", MB_ICONWARNING); + return 0; +} + LRESULT CMainFrame::WndProc(UINT msg, WPARAM wparam, LPARAM lparam) { try { switch (msg) { case WM_GETMINMAXINFO: return OnGetMinMaxInfo(msg, wparam, lparam); - case UWM_QUEUE_PROGRESS: return OnQueueProgress(wparam, lparam); + case UWM_QUEUE_PROGRESS: return OnQueueProgress(wparam, lparam); case UWM_QUEUE_DONE: return OnQueueDone(wparam, lparam); + case UWM_TRANSFORM_PROGRESS: return OnTransformProgress(wparam, lparam); + case UWM_TRANSFORM_DONE: return OnTransformDone(wparam, lparam); case UWM_QUEUE_ITEM_CLICKED: { int idx = static_cast(wparam); if (m_pDockQueue) { diff --git a/packages/media/cpp/src/win/ui_next/Mainfrm.h b/packages/media/cpp/src/win/ui_next/Mainfrm.h index abed715b..f7feb04a 100644 --- a/packages/media/cpp/src/win/ui_next/Mainfrm.h +++ b/packages/media/cpp/src/win/ui_next/Mainfrm.h @@ -32,11 +32,16 @@ private: void OnAddFolder(); void OnClearQueue(); void OnResize(); + void OnTransform(); void OnExit(); LRESULT OnGetMinMaxInfo(UINT msg, WPARAM wparam, LPARAM lparam); LRESULT OnQueueProgress(WPARAM wparam, LPARAM lparam); LRESULT OnQueueDone(WPARAM wparam, LPARAM lparam); + LRESULT OnTransformProgress(WPARAM wparam, LPARAM lparam); + LRESULT OnTransformDone(WPARAM wparam, LPARAM lparam); + + std::vector GetSelectedQueueItems(); CImagePreview m_view; IUIRibbon* m_pIUIRibbon = nullptr; @@ -45,6 +50,7 @@ private: std::thread m_worker; bool m_processing = false; + std::wstring m_lastPrompt; }; #endif // PM_UI_MAINFRM_H diff --git a/packages/media/cpp/src/win/ui_next/Resource.h b/packages/media/cpp/src/win/ui_next/Resource.h index be2c869c..283f152b 100644 --- a/packages/media/cpp/src/win/ui_next/Resource.h +++ b/packages/media/cpp/src/win/ui_next/Resource.h @@ -25,10 +25,17 @@ #define IDC_CHK_AUTOROT 608 #define IDC_CHK_STRIP 609 +// AI Transform dialog controls +#define IDD_TRANSFORM_PROMPT 700 +#define IDC_EDIT_PROMPT 701 +#define IDC_STATIC_PROMPT_LABEL 702 + // User messages #define UWM_QUEUE_PROGRESS (WM_USER + 100) #define UWM_QUEUE_DONE (WM_USER + 101) #define UWM_IMAGELOADED (WM_USER + 102) #define UWM_QUEUE_ITEM_CLICKED (WM_USER + 103) +#define UWM_TRANSFORM_PROGRESS (WM_USER + 104) +#define UWM_TRANSFORM_DONE (WM_USER + 105) #endif // PM_UI_RESOURCE_H diff --git a/packages/media/cpp/src/win/ui_next/RibbonUI.bml b/packages/media/cpp/src/win/ui_next/RibbonUI.bml new file mode 100644 index 00000000..8bc455a1 Binary files /dev/null and b/packages/media/cpp/src/win/ui_next/RibbonUI.bml differ diff --git a/packages/media/cpp/src/win/ui_next/RibbonUI.h b/packages/media/cpp/src/win/ui_next/RibbonUI.h index 7b5cdead..07e82d06 100644 --- a/packages/media/cpp/src/win/ui_next/RibbonUI.h +++ b/packages/media/cpp/src/win/ui_next/RibbonUI.h @@ -30,6 +30,14 @@ #define IDC_CMD_EXIT_LabelTitle_RESID 3041 #define IDC_CMD_ABOUT 305 #define IDC_CMD_ABOUT_LabelTitle_RESID 3051 +#define cmdTabAI 410 +#define cmdTabAI_LabelTitle_RESID 4101 +#define cmdGroupTransform 411 +#define cmdGroupTransform_LabelTitle_RESID 4111 +#define IDC_CMD_TRANSFORM 310 +#define IDC_CMD_TRANSFORM_LabelTitle_RESID 3101 +#define IDC_CMD_TRANSFORM_SmallImages_RESID 3103 +#define IDC_CMD_TRANSFORM_LargeImages_RESID 3102 #define cmdAppMenu 710 #define IDC_RIBBONHELP 700 #define IDC_QAT 701 diff --git a/packages/media/cpp/src/win/ui_next/RibbonUI.rc b/packages/media/cpp/src/win/ui_next/RibbonUI.rc index 4225a463..415009cb 100644 --- a/packages/media/cpp/src/win/ui_next/RibbonUI.rc +++ b/packages/media/cpp/src/win/ui_next/RibbonUI.rc @@ -57,4 +57,21 @@ BEGIN IDC_CMD_ABOUT_LabelTitle_RESID L"About" /* LabelTitle IDC_CMD_ABOUT_LabelTitle_RESID: (null) */ END -APPLICATION_RIBBON UIFILE "C:/Users/zx/Desktop/polymech/polymech-mono/packages/media/cpp/src/win/ui_next/Ribbon.bml" +STRINGTABLE +BEGIN + cmdTabAI_LabelTitle_RESID L"AI Transform" /* LabelTitle cmdTabAI_LabelTitle_RESID: (null) */ +END + +STRINGTABLE +BEGIN + cmdGroupTransform_LabelTitle_RESID L"Transform" /* LabelTitle cmdGroupTransform_LabelTitle_RESID: (null) */ +END + +STRINGTABLE +BEGIN + IDC_CMD_TRANSFORM_LabelTitle_RESID L"Transform" /* LabelTitle IDC_CMD_TRANSFORM_LabelTitle_RESID: (null) */ +END + +IDC_CMD_TRANSFORM_SmallImages_RESID BITMAP "res\\TransformS.bmp" /* SmallImages IDC_CMD_TRANSFORM_SmallImages_RESID: (null) */ +IDC_CMD_TRANSFORM_LargeImages_RESID BITMAP "res\\TransformL.bmp" /* LargeImages IDC_CMD_TRANSFORM_LargeImages_RESID: (null) */ +APPLICATION_RIBBON UIFILE "C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\media\\cpp\\src\\win\\ui_next\\RibbonUI.bml" diff --git a/packages/media/cpp/tests/assets/in/DSC01177_remove_background_white_studio_product_s.JPG b/packages/media/cpp/tests/assets/in/DSC01177_remove_background_white_studio_product_s.JPG index f3a078bf..9b55235d 100644 Binary files a/packages/media/cpp/tests/assets/in/DSC01177_remove_background_white_studio_product_s.JPG and b/packages/media/cpp/tests/assets/in/DSC01177_remove_background_white_studio_product_s.JPG differ