media:cpp : installer | explorer shellext
This commit is contained in:
parent
74983ff125
commit
39388eb5d9
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
BIN
packages/media/cpp/dist/media-img-Setup.exe
vendored
Normal file
BIN
packages/media/cpp/dist/media-img-Setup.exe
vendored
Normal file
Binary file not shown.
BIN
packages/media/cpp/dist/media-img.exe
vendored
BIN
packages/media/cpp/dist/media-img.exe
vendored
Binary file not shown.
BIN
packages/media/cpp/dist/media-img.pdb
vendored
BIN
packages/media/cpp/dist/media-img.pdb
vendored
Binary file not shown.
BIN
packages/media/cpp/dist/url-test_400.png
vendored
Normal file
BIN
packages/media/cpp/dist/url-test_400.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
274
packages/media/cpp/installer.nsi
Normal file
274
packages/media/cpp/installer.nsi
Normal 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
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
375
packages/media/cpp/src/win/register_explorer.cpp
Normal file
375
packages/media/cpp/src/win/register_explorer.cpp
Normal 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
|
||||
25
packages/media/cpp/src/win/register_explorer.hpp
Normal file
25
packages/media/cpp/src/win/register_explorer.hpp
Normal 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
|
||||
BIN
packages/media/cpp/tests/assets/in/DSC01177_converted.jpg
Normal file
BIN
packages/media/cpp/tests/assets/in/DSC01177_converted.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 477 KiB |
Loading…
Reference in New Issue
Block a user