media:cpp : installer | explorer shellext

This commit is contained in:
lovebird 2026-04-13 22:42:55 +02:00
parent 74983ff125
commit 39388eb5d9
12 changed files with 751 additions and 0 deletions

View File

@ -144,6 +144,9 @@ add_executable(media-img
src/http/serve.cpp
src/ipc/ipc_serve.cpp
)
if(WIN32)
target_sources(media-img PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/win/register_explorer.cpp")
endif()
target_include_directories(media-img PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src

View File

@ -50,6 +50,36 @@ cmake --build --preset release
Binary: **`dist/media-img`** (`.exe` on Windows).
### Windows installer (NSIS)
From `packages/media/cpp`, after a release build:
```bash
npm run build:installer
```
Produces **`dist/media-img-Setup.exe`**, installs `media-img.exe`, libvips DLLs, `vips-modules-8.18`, and `scripts/explorer-resize.ps1`, and prepends the install directory to the **user** `PATH`. Uninstaller is registered under Add/Remove Programs.
### Explorer context menu (`media-img register-explorer`)
Implemented in **C++** (Windows registry). The NSIS installer runs `media-img.exe register-explorer` after copying files; uninstall runs `register-explorer --unregister --no-refresh-shell`.
```bash
media-img register-explorer
media-img register-explorer --dry
media-img register-explorer --unregister
```
Registers a **PM-Media** cascading menu on image extensions (including **`.avif`**, **`.arw`**, **`.webp`**, TIFF, etc.), on **folders**, and on the **folder background** (empty area). **Resize** presets (default widths **1980, 1200, 800, 400**): **in place** or **copy** as `${SRC_NAME}_${width}${SRC_EXT}`. **Convert to JPG** writes `${SRC_NAME}_converted.jpg` via `explorer-convert.ps1`. Override widths with `--widths 1920,800`.
Under `SystemFileAssociations`, each extension is registered as **`.ext`**, **`.EXT`**, and **`.Ext`** so Explorer picks up the menu regardless of filename casing.
Defaults: **`--media-bin`** = this executable; **`--explorer-script`** / **`--explorer-convert-script`** = `<exe_dir>\\scripts\\explorer-*.ps1`, or `packages/media/scripts\\…` from a dev `cpp\\dist` build.
`pm-media register-explorer` forwards argv to `media-img.exe` for convenience.
Context menus run `powershell.exe -WindowStyle Hidden`; re-run `media-img register-explorer` after updates so registry command lines refresh.
## Formats — same idea as Sharp / libvips
Sharp wraps libvips: **decode → process → encode**. We do the same with `vips_image_new_from_file` and format-specific savers.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
packages/media/cpp/dist/url-test_400.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,274 @@
; ============================================================================
; PolyMech media-img — NSIS installer
; ============================================================================
; From packages/media/cpp:
; npm run build:installer
; Output: dist\media-img-Setup.exe
; Requires: NSIS 3+, CMake release build (dist\media-img.exe + vips DLLs)
; ============================================================================
!include "MUI2.nsh"
!include "FileFunc.nsh"
!include "WinMessages.nsh"
; ---------------------------------------------------------------------------
; Self-contained string helpers (PATH edit)
; ---------------------------------------------------------------------------
!macro _StrContainsConstructor un
Function ${un}StrContains
Exch $R1 ; SubString
Exch 1
Exch $R0 ; String
Push $R2
Push $R3
Push $R4
StrLen $R2 $R0
StrLen $R3 $R1
StrCpy $R4 0
loop:
IntCmp $R4 $R2 notfound 0 notfound
StrCpy $R5 $R0 $R3 $R4
StrCmp $R5 $R1 found
IntOp $R4 $R4 + 1
Goto loop
found:
StrCpy $R0 $R1
Goto done
notfound:
StrCpy $R0 ""
done:
Pop $R4
Pop $R3
Pop $R2
Pop $R1
Exch $R0
FunctionEnd
!macroend
!insertmacro _StrContainsConstructor ""
!macro StrContains ResultVar String SubString
Push `${String}`
Push `${SubString}`
Call StrContains
Pop `${ResultVar}`
!macroend
!macro _StrReplaceConstructor un
Function ${un}StrReplace
Exch $R2 ; New
Exch 1
Exch $R1 ; Old
Exch 2
Exch $R0 ; String
Push $R3
Push $R4
Push $R5
Push $R6
Push $R7
Push $R8
Push $R9
StrCpy $R3 0
StrLen $R4 $R1
StrLen $R6 $R0
StrLen $R9 $R2
loop:
StrCpy $R5 $R0 $R4 $R3
StrCmp $R5 $R1 found
StrCmp $R3 $R6 done
IntOp $R3 $R3 + 1
Goto loop
found:
StrCpy $R5 $R0 $R3
IntOp $R8 $R3 + $R4
StrCpy $R7 $R0 "" $R8
StrCpy $R0 "$R5$R2$R7"
StrLen $R6 $R0
IntOp $R3 $R3 + $R9
Goto loop
done:
Pop $R9
Pop $R8
Pop $R7
Pop $R6
Pop $R5
Pop $R4
Pop $R3
Push $R0
Exch 3
Pop $R1
Pop $R0
Pop $R2
FunctionEnd
!macroend
!insertmacro _StrReplaceConstructor ""
!insertmacro _StrReplaceConstructor "un."
!macro StrReplace ResultVar String Old New
Push `${String}`
Push `${Old}`
Push `${New}`
!ifdef __UNINSTALL__
Call un.StrReplace
!else
Call StrReplace
!endif
Pop `${ResultVar}`
!macroend
; ---------------------------------------------------------------------------
; Metadata
; ---------------------------------------------------------------------------
!define PRODUCT_NAME "PolyMech media-img"
!define PRODUCT_VERSION "0.1.0"
!define PRODUCT_PUBLISHER "PolyMech"
!define PRODUCT_WEB_SITE "https://git.polymech.info/mono"
!define PRODUCT_EXE "media-img.exe"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
!define ENV_KEY "Environment"
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "dist\media-img-Setup.exe"
InstallDir ""
RequestExecutionLevel user
Var IsAdmin
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico"
!define MUI_ABORTWARNING
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
Function .onInit
UserInfo::GetAccountType
Pop $0
StrCmp $0 "Admin" 0 not_admin
StrCpy $IsAdmin "1"
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCT_NAME}"
Goto init_done
not_admin:
StrCpy $IsAdmin "0"
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCT_NAME}"
init_done:
FunctionEnd
Section "Install"
SetOutPath "$INSTDIR"
File "dist\${PRODUCT_EXE}"
File /nonfatal "dist\*.dll"
; libvips optional modules (HEIF, etc.) — copied next to media-img by CMake
SetOutPath "$INSTDIR\vips-modules-8.18"
File /r "dist\vips-modules-8.18\*.*"
SetOutPath "$INSTDIR\scripts"
File "..\scripts\explorer-resize.ps1"
File "..\scripts\explorer-convert.ps1"
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\Uninstall.exe"
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_EXE}" "--help"
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
StrCmp $IsAdmin "1" 0 reg_user
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}"
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "InstallLocation" "$INSTDIR"
WriteRegDWORD HKLM "${PRODUCT_UNINST_KEY}" "NoModify" 1
WriteRegDWORD HKLM "${PRODUCT_UNINST_KEY}" "NoRepair" 1
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${PRODUCT_UNINST_KEY}" "EstimatedSize" $0
WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "InstallMode" "AllUsers"
Goto reg_done
reg_user:
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "InstallLocation" "$INSTDIR"
WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "NoModify" 1
WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "NoRepair" 1
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "EstimatedSize" $0
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "InstallMode" "CurrentUser"
reg_done:
Call AddToPath
DetailPrint "Registering Explorer context menu (media-img register-explorer)..."
ExecWait '"$INSTDIR\${PRODUCT_EXE}" register-explorer' $0
SectionEnd
Section "Uninstall"
DetailPrint "Removing Explorer context menu..."
IfFileExists "$INSTDIR\${PRODUCT_EXE}" 0 skip_unreg_explorer
ExecWait '"$INSTDIR\${PRODUCT_EXE}" register-explorer --unregister --no-refresh-shell' $0
skip_unreg_explorer:
Delete "$INSTDIR\${PRODUCT_EXE}"
Delete "$INSTDIR\*.dll"
RMDir /r "$INSTDIR\vips-modules-8.18"
Delete "$INSTDIR\scripts\explorer-resize.ps1"
Delete "$INSTDIR\scripts\explorer-convert.ps1"
RMDir "$INSTDIR\scripts"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
ReadRegStr $0 HKLM "${PRODUCT_UNINST_KEY}" "InstallMode"
StrCmp $0 "AllUsers" 0 unreg_user
DeleteRegKey HKLM "${PRODUCT_UNINST_KEY}"
Goto unreg_done
unreg_user:
DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}"
unreg_done:
Call un.RemoveFromPath
SectionEnd
Function AddToPath
ReadRegStr $0 HKCU "${ENV_KEY}" "Path"
!insertmacro StrContains $1 "$0" "$INSTDIR"
StrCmp $1 "" 0 already_in_path
StrCpy $0 "$0;$INSTDIR"
WriteRegExpandStr HKCU "${ENV_KEY}" "Path" "$0"
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000
already_in_path:
FunctionEnd
Function un.RemoveFromPath
ReadRegStr $0 HKCU "${ENV_KEY}" "Path"
!insertmacro StrReplace $1 "$0" ";$INSTDIR" ""
StrCmp $1 "$0" 0 write_path
!insertmacro StrReplace $1 "$0" "$INSTDIR;" ""
StrCmp $1 "$0" 0 write_path
!insertmacro StrReplace $1 "$0" "$INSTDIR" ""
StrCmp $1 "$0" done_path
write_path:
WriteRegExpandStr HKCU "${ENV_KEY}" "Path" "$1"
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000
done_path:
FunctionEnd

View File

@ -11,6 +11,7 @@
"config:release": "cmake --preset release",
"build": "cmake --preset dev && cmake --build --preset dev",
"build:release": "cmake --preset release && cmake --build --preset release",
"build:installer": "npm run build:release && makensis -V2 installer.nsi",
"clean": "cmake -E rm -rf build dist",
"rebuild": "npm run clean && npm run build",
"generate:assets": "node tests/assets/build-fixtures.mjs",

View File

@ -17,6 +17,9 @@
#include "core/resize.hpp"
#include "http/serve.hpp"
#include "ipc/ipc_serve.hpp"
#if defined(_WIN32)
#include "win/register_explorer.hpp"
#endif
#ifndef MEDIA_IMG_VERSION
#define MEDIA_IMG_VERSION "0.1.0"
@ -142,6 +145,28 @@ int main(int argc, char **argv) {
auto *kbot_cmd = app.add_subcommand("kbot", "Forward remaining args to kbot (KBOT_EXE)");
kbot_cmd->allow_extras(true);
std::string reg_group = "PM-Media";
bool reg_unregister = false;
bool reg_dry = false;
bool reg_no_refresh = false;
std::string reg_media_bin;
std::string reg_explorer_script;
std::string reg_explorer_convert_script;
std::string reg_widths = "1980,1200,800,400";
auto *reg_cmd = app.add_subcommand(
"register-explorer",
"Register Windows Explorer menus: resize presets + Convert to JPG (explorer-*.ps1 scripts)");
reg_cmd->add_option("--group", reg_group)->default_val("PM-Media");
reg_cmd->add_flag("--unregister", reg_unregister);
reg_cmd->add_flag("--dry", reg_dry);
reg_cmd->add_flag("--no-refresh-shell", reg_no_refresh);
reg_cmd->add_option("--media-bin", reg_media_bin, "Path to media-img.exe (default: this executable)");
reg_cmd->add_option("--explorer-script", reg_explorer_script,
"Path to explorer-resize.ps1 (default: <exe_dir>\\scripts\\explorer-resize.ps1)");
reg_cmd->add_option("--explorer-convert-script", reg_explorer_convert_script,
"Path to explorer-convert.ps1 (default: <exe_dir>\\scripts\\explorer-convert.ps1)");
reg_cmd->add_option("--widths", reg_widths)->default_val("1980,1200,800,400");
CLI11_PARSE(app, argc, argv);
if (resize_cmd->parsed()) {
@ -224,6 +249,24 @@ int main(int argc, char **argv) {
return forward_kbot(kbot_cmd->remaining());
}
if (reg_cmd->parsed()) {
#if defined(_WIN32)
media::win::RegisterExplorerOptions o;
o.group = reg_group;
o.unregister = reg_unregister;
o.dry = reg_dry;
o.refresh_shell = !reg_no_refresh;
o.media_bin = reg_media_bin;
o.explorer_script = reg_explorer_script;
o.explorer_convert_script = reg_explorer_convert_script;
o.widths = reg_widths;
return media::win::register_explorer_run(o);
#else
std::cerr << "media-img: register-explorer is only available on Windows.\n";
return 1;
#endif
}
std::cout << app.help() << "\n";
return 0;
}

View File

@ -0,0 +1,375 @@
#include "register_explorer.hpp"
#include <algorithm>
#include <cctype>
#include <iostream>
#include <set>
#include <string>
#include <vector>
#include <windows.h>
#pragma comment(lib, "Advapi32.lib")
namespace media::win {
namespace {
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<int>(s.size()), nullptr, 0);
if (n <= 0)
return L"";
std::wstring w(static_cast<size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), w.data(), n);
return w;
}
std::string wide_to_utf8(const std::wstring &w) {
if (w.empty())
return {};
int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
if (n <= 0)
return {};
std::string s(static_cast<size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
return s;
}
std::wstring exe_directory() {
wchar_t buf[MAX_PATH]{};
DWORD n = GetModuleFileNameW(nullptr, buf, MAX_PATH);
if (n == 0 || n >= MAX_PATH)
return L".";
std::wstring p(buf, n);
const size_t pos = p.find_last_of(L"\\/");
if (pos == std::wstring::npos)
return L".";
return p.substr(0, pos);
}
std::wstring default_media_bin() {
wchar_t buf[MAX_PATH]{};
DWORD n = GetModuleFileNameW(nullptr, buf, MAX_PATH);
if (n == 0 || n >= MAX_PATH)
return L"media-img.exe";
return std::wstring(buf, n);
}
bool file_exists_w(const std::wstring &path) {
const DWORD a = GetFileAttributesW(path.c_str());
return a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
std::wstring normalize_path(const std::wstring &p) {
wchar_t buf[MAX_PATH]{};
DWORD n = GetFullPathNameW(p.c_str(), MAX_PATH, buf, nullptr);
if (n == 0 || n >= MAX_PATH)
return p;
return std::wstring(buf, n);
}
std::wstring default_script_path(const std::wstring &exe_dir, const wchar_t *filename) {
const std::wstring candidates[] = {
exe_dir + L"\\scripts\\" + filename,
exe_dir + L"\\..\\..\\scripts\\" + filename,
};
for (const auto &c : candidates) {
const std::wstring full = normalize_path(c);
if (file_exists_w(full))
return full;
}
return normalize_path(candidates[0]);
}
std::wstring default_explorer_script(const std::wstring &exe_dir) {
return default_script_path(exe_dir, L"explorer-resize.ps1");
}
std::wstring default_explorer_convert_script(const std::wstring &exe_dir) {
return default_script_path(exe_dir, L"explorer-convert.ps1");
}
bool parse_widths(const std::string &s, std::vector<int> &out) {
out.clear();
std::string cur;
auto flush = [&]() {
if (cur.empty())
return;
try {
int v = std::stoi(cur);
if (v > 0)
out.push_back(v);
} catch (...) {
}
cur.clear();
};
for (char c : s) {
if (c == ',' || c == ' ' || c == '\t') {
flush();
} else {
cur.push_back(c);
}
}
flush();
return !out.empty();
}
/** Lowercase ASCII extension without dot, e.g. "jpg", "tiff", "arw". */
static const char *k_canonical_ext[] = {"jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "tif",
"jpe", "jfif", "avif", "arw"};
/** Register .ext, .EXT, and .Ext so Explorer matches case-insensitively per user preference. */
std::vector<std::wstring> ext_dot_variants(const std::string &canon_lower_ascii) {
std::wstring lower = L".";
std::wstring upper = L".";
std::wstring title = L".";
for (unsigned char ch : canon_lower_ascii) {
lower += static_cast<wchar_t>(ch);
upper += static_cast<wchar_t>(std::toupper(ch));
}
if (!canon_lower_ascii.empty()) {
title += static_cast<wchar_t>(std::toupper(static_cast<unsigned char>(canon_lower_ascii[0])));
for (size_t i = 1; i < canon_lower_ascii.size(); ++i)
title += static_cast<wchar_t>(canon_lower_ascii[i]);
}
std::vector<std::wstring> v;
v.push_back(lower);
v.push_back(upper);
v.push_back(title);
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
return v;
}
struct AssocTarget {
std::wstring classes_suffix;
std::wstring path_token;
};
LONG create_key(HKEY root, const std::wstring &rel, HKEY *out) {
return RegCreateKeyExW(root, rel.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE,
KEY_READ | KEY_WRITE, nullptr, out, nullptr);
}
bool set_sz(HKEY key, const wchar_t *name, const std::wstring &value) {
const BYTE *data = reinterpret_cast<const BYTE *>(value.c_str());
const DWORD cb = static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t));
return RegSetValueExW(key, name, 0, REG_SZ, data, cb) == ERROR_SUCCESS;
}
bool set_default_sz(HKEY key, const std::wstring &value) {
const BYTE *data = reinterpret_cast<const BYTE *>(value.c_str());
const DWORD cb = static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t));
return RegSetValueExW(key, nullptr, 0, REG_SZ, data, cb) == ERROR_SUCCESS;
}
std::wstring build_resize_command_line(const std::wstring &script, const std::wstring &media,
const std::wstring &path_token, int w, bool inplace) {
std::wstring cmd =
L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + script +
L"\" -MediaImg \"" + media + L"\" -Path \"" + path_token + L"\" -MaxWidth " + std::to_wstring(w) +
L" -Mode ";
cmd += inplace ? L"InPlace" : L"Copy";
return cmd;
}
std::wstring build_convert_command_line(const std::wstring &convert_script, const std::wstring &media,
const std::wstring &path_token) {
return L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + convert_script +
L"\" -MediaImg \"" + media + L"\" -Path \"" + path_token + L"\"";
}
bool write_verbed_command(HKEY root, const std::wstring &shell_group, const std::wstring &id,
const std::wstring &mui_name, const std::wstring &command_line) {
const std::wstring sub = shell_group + L"\\shell\\" + id;
HKEY h_cmd = nullptr;
if (create_key(root, sub, &h_cmd) != ERROR_SUCCESS)
return false;
if (!set_sz(h_cmd, L"MUIVerb", mui_name)) {
RegCloseKey(h_cmd);
return false;
}
RegCloseKey(h_cmd);
const std::wstring sub_cmd = sub + L"\\command";
HKEY h_run = nullptr;
if (create_key(root, sub_cmd, &h_run) != ERROR_SUCCESS)
return false;
const bool ok = set_default_sz(h_run, command_line);
RegCloseKey(h_run);
return ok;
}
bool register_one_target(HKEY root, const std::wstring &classes_suffix, const std::wstring &path_token,
const std::wstring &group, const std::wstring &resize_script,
const std::wstring &convert_script, const std::wstring &media, const std::vector<int> &widths) {
const std::wstring base = std::wstring(L"Software\\Classes\\") + classes_suffix;
const std::wstring shell_group = base + L"\\shell\\" + group;
HKEY h_shell = nullptr;
if (create_key(root, shell_group, &h_shell) != ERROR_SUCCESS)
return false;
const bool head_ok = set_sz(h_shell, L"MUIVerb", group) && set_sz(h_shell, L"subCommands", L"");
RegCloseKey(h_shell);
if (!head_ok)
return false;
for (int w : widths) {
const std::wstring id_in = L"r" + std::to_wstring(w) + L"in";
const std::wstring id_cp = L"r" + std::to_wstring(w) + L"cp";
const std::wstring name_in = L"Resize max " + std::to_wstring(w) + L" (in place)";
const std::wstring name_cp = L"Resize max " + std::to_wstring(w) + L" (copy _" + std::to_wstring(w) + L")";
auto write_resize = [&](const std::wstring &id, const std::wstring &mui_name, bool inplace) -> bool {
return write_verbed_command(root, shell_group, id, mui_name,
build_resize_command_line(resize_script, media, path_token, w, inplace));
};
if (!write_resize(id_in, name_in, true))
return false;
if (!write_resize(id_cp, name_cp, false))
return false;
}
const std::wstring cvt_line = build_convert_command_line(convert_script, media, path_token);
if (!write_verbed_command(root, shell_group, L"cvtjpg", L"Convert to JPG", cvt_line))
return false;
return true;
}
void refresh_shell_notify() {
using SHChangeNotify_fn = void(WINAPI *)(LONG, UINT, const void *, const void *);
HMODULE sh = LoadLibraryW(L"shell32.dll");
if (!sh)
return;
auto fn = reinterpret_cast<SHChangeNotify_fn>(GetProcAddress(sh, "SHChangeNotify"));
if (fn)
fn(0x08000000, 0, nullptr, nullptr);
FreeLibrary(sh);
}
std::vector<AssocTarget> build_file_association_targets() {
std::vector<AssocTarget> out;
for (const char *c : k_canonical_ext) {
const std::string canon(c);
for (const std::wstring &dot : ext_dot_variants(canon)) {
AssocTarget t;
t.classes_suffix = L"SystemFileAssociations\\" + dot;
t.path_token = L"%1";
out.push_back(std::move(t));
}
}
out.push_back(AssocTarget{L"Directory", L"%1"});
out.push_back(AssocTarget{L"Directory\\Background", L"%V"});
return out;
}
std::wstring shell_group_rel(const std::wstring &classes_suffix, const std::wstring &group) {
return L"Software\\Classes\\" + classes_suffix + L"\\shell\\" + group;
}
} // namespace
int register_explorer_run(const RegisterExplorerOptions &opt) {
const std::wstring exe_dir = exe_directory();
std::wstring media_w = opt.media_bin.empty() ? default_media_bin() : utf8_to_wide(opt.media_bin);
std::wstring resize_script_w =
opt.explorer_script.empty() ? default_explorer_script(exe_dir) : utf8_to_wide(opt.explorer_script);
std::wstring convert_script_w = opt.explorer_convert_script.empty()
? default_explorer_convert_script(exe_dir)
: utf8_to_wide(opt.explorer_convert_script);
std::vector<int> widths;
if (!parse_widths(opt.widths, widths)) {
std::cerr << "register-explorer: invalid or empty --widths\n";
return 1;
}
if (!opt.unregister) {
if (!file_exists_w(media_w)) {
std::cerr << "register-explorer: media-img not found: " << wide_to_utf8(media_w) << "\n";
return 1;
}
if (!file_exists_w(resize_script_w)) {
std::cerr << "register-explorer: resize script not found: " << wide_to_utf8(resize_script_w) << "\n";
return 1;
}
if (!file_exists_w(convert_script_w)) {
std::cerr << "register-explorer: convert script not found: " << wide_to_utf8(convert_script_w) << "\n";
return 1;
}
}
const std::wstring group_w = utf8_to_wide(opt.group);
if (opt.unregister) {
if (opt.dry) {
std::cout << "Dry run: would remove shell keys for group " << opt.group << "\n";
return 0;
}
bool ok = true;
for (const char *c : k_canonical_ext) {
for (const std::wstring &dot : ext_dot_variants(std::string(c))) {
std::wstring rel = shell_group_rel(L"SystemFileAssociations\\" + dot, group_w);
const LONG r = RegDeleteTreeW(HKEY_CURRENT_USER, rel.c_str());
if (r != ERROR_SUCCESS && r != ERROR_FILE_NOT_FOUND)
ok = false;
}
}
{
std::wstring rel = shell_group_rel(L"Directory", group_w);
const LONG r = RegDeleteTreeW(HKEY_CURRENT_USER, rel.c_str());
if (r != ERROR_SUCCESS && r != ERROR_FILE_NOT_FOUND)
ok = false;
}
{
std::wstring rel = shell_group_rel(L"Directory\\Background", group_w);
const LONG r = RegDeleteTreeW(HKEY_CURRENT_USER, rel.c_str());
if (r != ERROR_SUCCESS && r != ERROR_FILE_NOT_FOUND)
ok = false;
}
if (!ok) {
std::cerr << "register-explorer: failed to remove some registry keys\n";
return 1;
}
std::cout << "Removed Explorer menus for group " << opt.group << "\n";
if (opt.refresh_shell)
refresh_shell_notify();
return 0;
}
if (opt.dry) {
std::cout << "Dry run (sample for %1):\n";
const std::wstring sample_tok = L"%1";
for (int w : widths) {
std::cout << " " << wide_to_utf8(build_resize_command_line(resize_script_w, media_w, sample_tok, w, true))
<< "\n";
std::cout << " " << wide_to_utf8(build_resize_command_line(resize_script_w, media_w, sample_tok, w, false))
<< "\n";
}
std::cout << " " << wide_to_utf8(build_convert_command_line(convert_script_w, media_w, sample_tok)) << "\n";
std::cout << "Also register folder background with %V.\n";
std::cout << "File types: " << (sizeof(k_canonical_ext) / sizeof(k_canonical_ext[0]))
<< " canonical extensions × 3 case variants under SystemFileAssociations.\n";
return 0;
}
const std::vector<AssocTarget> targets = build_file_association_targets();
for (const auto &t : targets) {
if (!register_one_target(HKEY_CURRENT_USER, t.classes_suffix, t.path_token, group_w, resize_script_w,
convert_script_w, media_w, widths)) {
std::cerr << "register-explorer: failed to register a context-menu target\n";
return 1;
}
}
std::cout << "Registered Explorer menus for group " << opt.group << "\n";
if (opt.refresh_shell)
refresh_shell_notify();
return 0;
}
} // namespace media::win

View File

@ -0,0 +1,25 @@
#pragma once
#include <string>
namespace media::win {
struct RegisterExplorerOptions {
std::string group{"PM-Media"};
bool unregister{false};
bool dry{false};
bool refresh_shell{true};
/** Empty = use path of running media-img.exe */
std::string media_bin;
/** Empty = <exe_dir>\\scripts\\explorer-resize.ps1 */
std::string explorer_script;
/** Empty = <exe_dir>\\scripts\\explorer-convert.ps1 */
std::string explorer_convert_script;
/** Comma-separated positive integers */
std::string widths{"1980,1200,800,400"};
};
/** Windows only. Returns 0 on success. */
int register_explorer_run(const RegisterExplorerOptions &opt);
} // namespace media::win

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB