diff --git a/packages/media/cpp/CMakeLists.txt b/packages/media/cpp/CMakeLists.txt index 19a0373c..fd3c0b43 100644 --- a/packages/media/cpp/CMakeLists.txt +++ b/packages/media/cpp/CMakeLists.txt @@ -16,6 +16,11 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +# MSVC: UTF-8 source/execution charset so wide string literals (L"...") match UTF-8 in .cpp files. +if(MSVC) + add_compile_options(/utf-8) +endif() + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # Windows: official libvips dev tree (see scripts/fetch-vips-windows.ps1) — vips-dev-* under third_party/ @@ -145,7 +150,11 @@ add_executable(media-img src/ipc/ipc_serve.cpp ) if(WIN32) - target_sources(media-img PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/win/register_explorer.cpp") + target_sources(media-img PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src/win/register_explorer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/win/resize_ui.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/win/media-img-win.manifest" + ) endif() target_include_directories(media-img PRIVATE diff --git a/packages/media/cpp/README.md b/packages/media/cpp/README.md index b6b96e19..ea0a3e82 100644 --- a/packages/media/cpp/README.md +++ b/packages/media/cpp/README.md @@ -109,6 +109,8 @@ Sharp wraps libvips: **decode → process → encode**. We do the same with `vip | `flip` / `flop` | `flip` / `flop` | | | Letterbox | `background` | `#rrggbb` for `contain` | +**Windows:** `media-img resize --ui` opens a **native Win32** dialog to choose input/output paths, max dimensions, fit mode, quality, and enlargement / autorotate / strip options. Other CLI flags seed the dialog; **`--src` / `--dst` are not allowed** with `--ui`. If you cancel, the command exits without processing. + **REST** `POST /v1/resize` and **IPC** use the same JSON keys as the table, plus the fields in the **“Batch paths & cache”** section below. --- diff --git a/packages/media/cpp/dist/media-img.exe b/packages/media/cpp/dist/media-img.exe index 5864d7d0..9e40d072 100644 Binary files a/packages/media/cpp/dist/media-img.exe and b/packages/media/cpp/dist/media-img.exe differ diff --git a/packages/media/cpp/dist/media-img.pdb b/packages/media/cpp/dist/media-img.pdb index 3c90ab89..344e673a 100644 Binary files a/packages/media/cpp/dist/media-img.pdb and b/packages/media/cpp/dist/media-img.pdb differ diff --git a/packages/media/cpp/src/main.cpp b/packages/media/cpp/src/main.cpp index f25ae957..21a146c3 100644 --- a/packages/media/cpp/src/main.cpp +++ b/packages/media/cpp/src/main.cpp @@ -19,6 +19,7 @@ #include "ipc/ipc_serve.hpp" #if defined(_WIN32) #include "win/register_explorer.hpp" +#include "win/resize_ui.hpp" #endif #ifndef MEDIA_IMG_VERSION @@ -118,6 +119,12 @@ int main(int argc, char **argv) { ->add_option("--url-max-redirects", url_max_redirects, "Max redirects when fetching URL inputs") ->default_val(20); +#if defined(_WIN32) + bool resize_ui = false; + resize_cmd->add_flag("--ui", resize_ui, + "Windows: show native dialog to pick input/output and resize options (cannot be used with --src/--dst)"); +#endif + std::string host = "127.0.0.1"; int port = 8080; bool serve_no_cache = false; @@ -170,48 +177,103 @@ int main(int argc, char **argv) { CLI11_PARSE(app, argc, argv); if (resize_cmd->parsed()) { - const bool use_src_dst = !src_flag.empty() || !dst_flag.empty(); - if (use_src_dst) { - if (src_flag.empty() || dst_flag.empty()) { - std::cerr << "resize: --src and --dst must be used together\n"; + media::ResizeOptions opt; + std::string in_path_e; + std::string out_path_e; + +#if defined(_WIN32) + if (resize_ui) { + if (!src_flag.empty() || !dst_flag.empty()) { + std::cerr << "resize: --ui cannot be used with --src or --dst\n"; return 1; } - in_path = src_flag; - out_path = dst_flag; - } else if (in_path.empty()) { - std::cerr << "resize: provide input (and output, or omit output to write under the current directory)\n"; - return 1; - } else if (out_path.empty()) { + media::ResizeOptions initial{}; + initial.max_width = max_w; + initial.max_height = max_h; + initial.format = format; + initial.fit = fit; + initial.position = position; + initial.kernel = kernel; + initial.background = background; + initial.quality = quality; + initial.png_compression = png_compression; + initial.rotate = rotate; + initial.flip = flip; + initial.flop = flop; + initial.autorotate = !no_autorotate; + initial.strip_metadata = !no_strip; + initial.without_enlargement = !allow_enlargement; + initial.cache_enabled = !resize_no_cache; + initial.cache_dir = resize_cache_dir; + initial.url_timeout_sec = url_timeout_sec; + initial.url_max_redirects = url_max_redirects; + + std::string ui_in = in_path; + std::string ui_out = out_path; + if (!media::win::show_resize_ui(opt, ui_in, ui_out, initial)) + return 0; + in_path_e = std::move(ui_in); + out_path_e = std::move(ui_out); + } else +#endif + { + const bool use_src_dst = !src_flag.empty() || !dst_flag.empty(); + if (use_src_dst) { + if (src_flag.empty() || dst_flag.empty()) { + std::cerr << "resize: --src and --dst must be used together\n"; + return 1; + } + in_path_e = src_flag; + out_path_e = dst_flag; + } else if (in_path.empty()) { + std::cerr << "resize: provide input (and output, or omit output to write under the current directory)\n"; + return 1; + } else if (out_path.empty()) { + std::string derr; + out_path_e = media::default_output_path_for_resize(in_path, format, derr); + if (out_path_e.empty()) { + std::cerr << derr << "\n"; + return 1; + } + in_path_e = in_path; + } else { + in_path_e = in_path; + out_path_e = out_path; + } + + opt.max_width = max_w; + opt.max_height = max_h; + opt.format = format; + opt.fit = fit; + opt.position = position; + opt.kernel = kernel; + opt.background = background; + opt.quality = quality; + opt.png_compression = png_compression; + opt.rotate = rotate; + opt.flip = flip; + opt.flop = flop; + opt.autorotate = !no_autorotate; + opt.strip_metadata = !no_strip; + opt.without_enlargement = !allow_enlargement; + opt.cache_enabled = !resize_no_cache; + opt.cache_dir = resize_cache_dir; + opt.url_timeout_sec = url_timeout_sec; + opt.url_max_redirects = url_max_redirects; + } + + if (out_path_e.empty()) { std::string derr; - out_path = media::default_output_path_for_resize(in_path, format, derr); - if (out_path.empty()) { + out_path_e = media::default_output_path_for_resize(in_path_e, opt.format, derr); + if (out_path_e.empty()) { std::cerr << derr << "\n"; return 1; } } - media::ResizeOptions opt; - opt.max_width = max_w; - opt.max_height = max_h; - opt.format = format; - opt.fit = fit; - opt.position = position; - opt.kernel = kernel; - opt.background = background; - opt.quality = quality; - opt.png_compression = png_compression; - opt.rotate = rotate; - opt.flip = flip; - opt.flop = flop; - opt.autorotate = !no_autorotate; - opt.strip_metadata = !no_strip; - opt.without_enlargement = !allow_enlargement; - opt.cache_enabled = !resize_no_cache; - opt.cache_dir = resize_cache_dir; - opt.url_timeout_sec = url_timeout_sec; - opt.url_max_redirects = url_max_redirects; + std::string err; media::ResizeBatchResult batch; - if (!media::resize_batch(in_path, out_path, opt, err, &batch)) { + if (!media::resize_batch(in_path_e, out_path_e, opt, err, &batch)) { std::cerr << err << "\n"; return 1; } diff --git a/packages/media/cpp/src/win/media-img-win.manifest b/packages/media/cpp/src/win/media-img-win.manifest new file mode 100644 index 00000000..a7b4693c --- /dev/null +++ b/packages/media/cpp/src/win/media-img-win.manifest @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/packages/media/cpp/src/win/resize_ui.cpp b/packages/media/cpp/src/win/resize_ui.cpp new file mode 100644 index 00000000..cc854d42 --- /dev/null +++ b/packages/media/cpp/src/win/resize_ui.cpp @@ -0,0 +1,398 @@ +#include "resize_ui.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#pragma comment(lib, "Comdlg32.lib") +#pragma comment(lib, "User32.lib") +#pragma comment(lib, "Gdi32.lib") +#pragma comment(lib, "Comctl32.lib") + +namespace media::win { + +static std::string wide_to_utf8(const std::wstring &w) { + if (w.empty()) + return {}; + int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); + if (n <= 0) + return {}; + std::string s(static_cast(n), '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), s.data(), n, nullptr, nullptr); + return s; +} + +static std::wstring utf8_to_wide(const std::string &s) { + if (s.empty()) + return L""; + int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); + if (n <= 0) + return L""; + std::wstring w(static_cast(n), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), w.data(), n); + return w; +} + +enum : int { + IDC_STATIC_IN = 100, + IDC_EDIT_IN = 101, + IDC_BTN_IN = 102, + IDC_STATIC_OUT = 103, + IDC_EDIT_OUT = 104, + IDC_BTN_OUT = 105, + IDC_STATIC_MW = 106, + IDC_EDIT_MW = 107, + IDC_STATIC_MH = 108, + IDC_EDIT_MH = 109, + IDC_STATIC_FIT = 110, + IDC_COMBO_FIT = 111, + IDC_STATIC_Q = 112, + IDC_EDIT_Q = 113, + IDC_CHK_ENLARGE = 114, + IDC_CHK_AUTOROT = 115, + IDC_CHK_STRIP = 116, + IDC_BTN_OK = IDOK, + IDC_BTN_CANCEL = IDCANCEL, +}; + +static HFONT g_msg_font{}; + +static void ensure_message_font() { + if (g_msg_font) + return; + NONCLIENTMETRICSW ncm{}; + ncm.cbSize = sizeof(ncm); + if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) + g_msg_font = CreateFontIndirectW(&ncm.lfMessageFont); + if (!g_msg_font) + g_msg_font = static_cast(GetStockObject(DEFAULT_GUI_FONT)); +} + +static BOOL CALLBACK set_child_font(HWND child, LPARAM font) { + SendMessageW(child, WM_SETFONT, static_cast(font), TRUE); + return TRUE; +} + +static void apply_message_font(HWND root) { + ensure_message_font(); + SendMessageW(root, WM_SETFONT, reinterpret_cast(g_msg_font), TRUE); + EnumChildWindows(root, set_child_font, reinterpret_cast(g_msg_font)); +} + +struct UiState { + HWND root{}; + HWND h_in{}; + HWND h_out{}; + HWND h_mw{}; + HWND h_mh{}; + HWND h_fit{}; + HWND h_q{}; + HWND h_enlarge{}; + HWND h_autorot{}; + HWND h_strip{}; + ResizeOptions *opt{}; + std::string *in_path{}; + std::string *out_path{}; + bool accepted{false}; +}; + +static void set_utf8_edit(HWND h, const std::string &utf8) { + SetWindowTextW(h, utf8_to_wide(utf8).c_str()); +} + +static std::string get_utf8_edit(HWND h) { + const int n = GetWindowTextLengthW(h); + if (n <= 0) + return {}; + std::wstring w(static_cast(n) + 1, L'\0'); + GetWindowTextW(h, w.data(), n + 1); + w.resize(static_cast(n)); + return wide_to_utf8(w); +} + +static void browse_open(HWND owner, HWND h_edit) { + wchar_t buf[MAX_PATH * 4]{}; + OPENFILENAMEW of{}; + of.lStructSize = sizeof(of); + of.hwndOwner = owner; + of.lpstrFilter = L"Images\0*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic;*.jpe;*.jfif\0" + L"All\0*.*\0\0"; + of.nFilterIndex = 1; + of.lpstrFile = buf; + of.nMaxFile = MAX_PATH * 4; + of.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER; + if (GetOpenFileNameW(&of)) + SetWindowTextW(h_edit, buf); +} + +static void browse_save(HWND owner, HWND h_edit) { + wchar_t buf[MAX_PATH * 4]{}; + GetWindowTextW(h_edit, buf, MAX_PATH * 4); + OPENFILENAMEW of{}; + of.lStructSize = sizeof(of); + of.hwndOwner = owner; + of.lpstrFilter = L"JPEG\0*.jpg;*.jpeg\0PNG\0*.png\0WebP\0*.webp\0TIFF\0*.tif;*.tiff\0All\0*.*\0\0"; + of.nFilterIndex = 1; + of.lpstrFile = buf; + of.nMaxFile = MAX_PATH * 4; + of.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_EXPLORER; + if (GetSaveFileNameW(&of)) + SetWindowTextW(h_edit, buf); +} + +static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + UiState *st = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (msg) { + case WM_CREATE: { + auto *cs = reinterpret_cast(lp); + st = reinterpret_cast(cs->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(st)); + st->root = hwnd; + const int pad = 12; + const int x0 = 20; + int y = pad + 6; + const int lw = 128; + const int gap = 10; + const int ew = 340; + const int btn_w = 96; + const int btn_h = 28; + const int eh = 24; + const int edit_x = x0 + lw + gap; + const int browse_x = edit_x + ew + gap; + + CreateWindowExW(0, L"STATIC", L"Input image:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, + (HMENU)IDC_STATIC_IN, nullptr, nullptr); + st->h_in = CreateWindowExW( + WS_EX_CLIENTEDGE, L"EDIT", L"", + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 4, hwnd, (HMENU)IDC_EDIT_IN, nullptr, + nullptr); + CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, btn_w, btn_h, + hwnd, (HMENU)IDC_BTN_IN, nullptr, nullptr); + y += 44; + + CreateWindowExW(0, L"STATIC", L"Output (optional):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, + (HMENU)IDC_STATIC_OUT, nullptr, nullptr); + st->h_out = CreateWindowExW( + WS_EX_CLIENTEDGE, L"EDIT", L"", + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 4, hwnd, (HMENU)IDC_EDIT_OUT, + nullptr, nullptr); + CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, btn_w, btn_h, + hwnd, (HMENU)IDC_BTN_OUT, nullptr, nullptr); + y += 48; + + CreateWindowExW(0, L"STATIC", L"Max width:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, + (HMENU)IDC_STATIC_MW, nullptr, nullptr); + wchar_t mw[32]{}; + swprintf_s(mw, L"%d", st->opt->max_width); + st->h_mw = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", mw, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 108, eh + 4, hwnd, + (HMENU)IDC_EDIT_MW, nullptr, nullptr); + + const int mh_label_x = edit_x + 108 + gap + 24; + CreateWindowExW(0, L"STATIC", L"Max height:", WS_CHILD | WS_VISIBLE, mh_label_x, y + 2, 100, 20, hwnd, + (HMENU)IDC_STATIC_MH, nullptr, nullptr); + wchar_t mh[32]{}; + swprintf_s(mh, L"%d", st->opt->max_height); + st->h_mh = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", mh, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, mh_label_x + 100 + gap, y, 108, eh + 4, hwnd, + (HMENU)IDC_EDIT_MH, nullptr, nullptr); + y += 44; + + CreateWindowExW(0, L"STATIC", L"Fit:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, (HMENU)IDC_STATIC_FIT, + nullptr, nullptr); + st->h_fit = CreateWindowExW(WS_EX_CLIENTEDGE, L"COMBOBOX", L"", + WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 2, 240, + 200, hwnd, (HMENU)IDC_COMBO_FIT, nullptr, nullptr); + const wchar_t *fits[] = {L"inside", L"cover", L"contain", L"fill", L"outside"}; + for (const wchar_t *f : fits) + SendMessageW(st->h_fit, CB_ADDSTRING, 0, reinterpret_cast(f)); + { + std::wstring cur = utf8_to_wide(st->opt->fit); + for (auto &ch : cur) { + if (ch >= L'A' && ch <= L'Z') + ch = static_cast(ch - L'A' + L'a'); + } + int sel = 0; + for (int i = 0; i < 5; ++i) { + if (cur == fits[i]) { + sel = i; + break; + } + } + SendMessageW(st->h_fit, CB_SETCURSEL, static_cast(sel), 0); + } + y += 40; + + CreateWindowExW(0, L"STATIC", L"Quality (1–100):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw + 48, 20, hwnd, + (HMENU)IDC_STATIC_Q, nullptr, nullptr); + wchar_t q[16]{}; + swprintf_s(q, L"%d", st->opt->quality); + st->h_q = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", q, + WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 72, eh + 4, hwnd, + (HMENU)IDC_EDIT_Q, nullptr, nullptr); + y += 44; + + st->h_enlarge = + CreateWindowExW(0, L"BUTTON", L"Allow enlargement", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0, y, 220, + 26, hwnd, (HMENU)IDC_CHK_ENLARGE, nullptr, nullptr); + SendMessageW(st->h_enlarge, BM_SETCHECK, + st->opt->without_enlargement ? BST_UNCHECKED : BST_CHECKED, 0); + st->h_autorot = + CreateWindowExW(0, L"BUTTON", L"EXIF autorotate", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0 + 240, y, 200, + 26, hwnd, (HMENU)IDC_CHK_AUTOROT, nullptr, nullptr); + SendMessageW(st->h_autorot, BM_SETCHECK, st->opt->autorotate ? BST_CHECKED : BST_UNCHECKED, 0); + y += 34; + st->h_strip = CreateWindowExW(0, L"BUTTON", L"Strip metadata on save", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, + x0, y, 280, 26, hwnd, (HMENU)IDC_CHK_STRIP, nullptr, nullptr); + SendMessageW(st->h_strip, BM_SETCHECK, st->opt->strip_metadata ? BST_CHECKED : BST_UNCHECKED, 0); + y += 42; + + const int btn_row_w = 100; + CreateWindowExW(0, L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, browse_x - btn_row_w - gap - btn_row_w, + y, btn_row_w, 32, hwnd, (HMENU)IDC_BTN_OK, nullptr, nullptr); + CreateWindowExW(0, L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x - btn_row_w, y, btn_row_w, + 32, hwnd, (HMENU)IDC_BTN_CANCEL, nullptr, nullptr); + + set_utf8_edit(st->h_in, *st->in_path); + set_utf8_edit(st->h_out, *st->out_path); + apply_message_font(hwnd); + return 0; + } + case WM_COMMAND: { + if (!st) + return 0; + const int id = LOWORD(wp); + if (id == IDC_BTN_IN) { + browse_open(hwnd, st->h_in); + return 0; + } + if (id == IDC_BTN_OUT) { + browse_save(hwnd, st->h_out); + return 0; + } + if (id == IDC_BTN_CANCEL) { + st->accepted = false; + DestroyWindow(hwnd); + return 0; + } + if (id == IDC_BTN_OK) { + *st->in_path = get_utf8_edit(st->h_in); + if (st->in_path->empty()) { + MessageBoxW(hwnd, L"Choose an input image.", L"media-img", MB_OK | MB_ICONWARNING); + return 0; + } + *st->out_path = get_utf8_edit(st->h_out); + + wchar_t bmw[32]{}; + GetWindowTextW(st->h_mw, bmw, 32); + wchar_t bmh[32]{}; + GetWindowTextW(st->h_mh, bmh, 32); + st->opt->max_width = _wtoi(bmw); + st->opt->max_height = _wtoi(bmh); + if (st->opt->max_width < 0) + st->opt->max_width = 0; + if (st->opt->max_height < 0) + st->opt->max_height = 0; + + const int fi = static_cast(SendMessageW(st->h_fit, CB_GETCURSEL, 0, 0)); + const wchar_t *fits[] = {L"inside", L"cover", L"contain", L"fill", L"outside"}; + if (fi >= 0 && fi < 5) + st->opt->fit = wide_to_utf8(fits[fi]); + + wchar_t bq[32]{}; + GetWindowTextW(st->h_q, bq, 32); + st->opt->quality = _wtoi(bq); + if (st->opt->quality < 1) + st->opt->quality = 1; + if (st->opt->quality > 100) + st->opt->quality = 100; + + st->opt->without_enlargement = + SendMessageW(st->h_enlarge, BM_GETCHECK, 0, 0) != BST_CHECKED; + st->opt->autorotate = SendMessageW(st->h_autorot, BM_GETCHECK, 0, 0) == BST_CHECKED; + st->opt->strip_metadata = SendMessageW(st->h_strip, BM_GETCHECK, 0, 0) == BST_CHECKED; + + st->accepted = true; + DestroyWindow(hwnd); + return 0; + } + return 0; + } + case WM_CLOSE: + if (st) + st->accepted = false; + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } +} + +static const wchar_t kClassName[] = L"MediaImgResizeUi"; + +bool show_resize_ui(ResizeOptions &opt, std::string &input_path, std::string &output_path, const ResizeOptions &initial) { + opt = initial; + + INITCOMMONCONTROLSEX icc{}; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_STANDARD_CLASSES | ICC_WIN95_CLASSES; + InitCommonControlsEx(&icc); + + static bool reg = false; + if (!reg) { + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = UiWndProc; + wc.hInstance = GetModuleHandleW(nullptr); + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = reinterpret_cast(COLOR_3DFACE + 1); + wc.lpszClassName = kClassName; + if (!RegisterClassExW(&wc)) { + if (GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + return false; + } + reg = true; + } + + UiState state{}; + state.opt = &opt; + state.in_path = &input_path; + state.out_path = &output_path; + + const int W = 620; + const int H = 452; + RECT r{}; + SystemParametersInfoW(SPI_GETWORKAREA, 0, &r, 0); + const int sx = r.left + ((r.right - r.left) - W) / 2; + const int sy = r.top + ((r.bottom - r.top) - H) / 2; + + HWND hwnd = + CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, kClassName, L"media-img — resize", + WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN, sx, sy, W, H, nullptr, nullptr, GetModuleHandleW(nullptr), + &state); + if (!hwnd) + return false; + + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + + MSG msg; + while (GetMessageW(&msg, nullptr, 0, 0) > 0) { + if (!IsDialogMessageW(hwnd, &msg)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + return state.accepted; +} + +} // namespace media::win diff --git a/packages/media/cpp/src/win/resize_ui.hpp b/packages/media/cpp/src/win/resize_ui.hpp new file mode 100644 index 00000000..c199aeef --- /dev/null +++ b/packages/media/cpp/src/win/resize_ui.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "core/resize.hpp" + +namespace media::win { + +/** + * Show a native Win32 dialog: pick input image, optional output path, and resize options. + * Seeds defaults from `initial` (e.g. CLI flags). On Cancel returns false. + */ +bool show_resize_ui(ResizeOptions &opt, std::string &input_path, std::string &output_path, + const ResizeOptions &initial); + +} // namespace media::win