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