#include #include #include #include namespace { std::string join_src_semicolons(const std::vector &v) { std::string s; for (size_t i = 0; i < v.size(); ++i) { if (i) s += ';'; s += v[i]; } return s; } } // namespace #include #include #ifdef _WIN32 #include #else #include #endif #include "core/output_path.hpp" #include "core/resize.hpp" #include "http/serve.hpp" #include "ipc/ipc_serve.hpp" #if defined(_WIN32) #include "core/glob_paths.hpp" #include "win/register_explorer.hpp" #include "win/resize_progress_ui.hpp" #include "win/resize_ui.hpp" #include "win/ui_singleton.hpp" #endif #ifndef MEDIA_IMG_VERSION #define MEDIA_IMG_VERSION "0.1.0" #endif static int forward_kbot(const std::vector &args) { const char *exe = std::getenv("KBOT_EXE"); if (exe == nullptr || exe[0] == '\0') { std::cerr << "KBOT_EXE is not set. Set it to the kbot binary path (e.g. packages/kbot/cpp/dist/kbot.exe).\n"; return 1; } std::vector storage; storage.emplace_back(exe); storage.insert(storage.end(), args.begin(), args.end()); std::vector argv; for (auto &s : storage) { argv.push_back(s.data()); } argv.push_back(nullptr); #ifdef _WIN32 const int rc = _spawnvp(_P_WAIT, exe, argv.data()); if (rc < 0) { perror("_spawnvp"); return 1; } return rc; #else execvp(exe, argv.data()); perror("execvp"); return 127; #endif } int main(int argc, char **argv) { dotenv::init(dotenv::Preserve); CLI::App app{"media-img — resize (CLI), serve (REST), ipc (JSON lines)", "media-img"}; app.set_version_flag("-v,--version", std::string(MEDIA_IMG_VERSION)); app.require_subcommand(1); std::string in_path; std::string out_path; std::vector src_list; std::string dst_flag; int max_w = 0; int max_h = 0; std::string format; std::string fit = "inside"; std::string position = "centre"; std::string kernel = "lanczos3"; std::string background = "#ffffff"; int quality = 85; int png_compression = 6; int rotate = 0; bool flip = false; bool flop = false; bool no_autorotate = false; bool no_strip = false; bool allow_enlargement = false; auto *resize_cmd = app.add_subcommand("resize", "Resize / transform an image (libvips, Sharp-like options)"); resize_cmd->add_option("input", in_path, "Input path, glob (*, ?, **), or http(s):// URL")->required(false); resize_cmd->add_option( "output", out_path, "Output file/dir, or omit when there is exactly one input → write under cwd (sanitized name)"); resize_cmd->add_option("--src", src_list, "Input (repeat for multiple); use with --dst; Explorer passes several files") ->expected(-1); resize_cmd->add_option("--dst", dst_flag, "Same as positional output; directory if multiple inputs"); resize_cmd->add_option("--max-width", max_w, "Target / max width (0 = no limit)"); resize_cmd->add_option("--max-height", max_h, "Target / max height (0 = no limit)"); resize_cmd->add_option("--format", format, "Output format (default: from extension)"); resize_cmd ->add_option("--fit", fit, "inside|cover|contain|fill|outside — see Sharp resize.fit") ->default_val("inside"); resize_cmd->add_option("--position", position, "For cover: centre|attention|entropy|…")->default_val("centre"); resize_cmd->add_option("--kernel", kernel, "nearest|cubic|mitchell|lanczos2|lanczos3")->default_val("lanczos3"); resize_cmd->add_option("-q,--quality", quality, "JPEG/WebP/AVIF quality 1–100")->default_val(85); resize_cmd->add_option("--png-compression", png_compression, "PNG DEFLATE 0–9")->default_val(6); resize_cmd->add_option("--background", background, "Letterbox colour #rrggbb (contain)"); resize_cmd->add_option("--rotate", rotate, "Rotate 0|90|180|270 after EXIF autorotate")->default_val(0); resize_cmd->add_flag("--flip", flip, "Vertical flip"); resize_cmd->add_flag("--flop", flop, "Horizontal flop"); resize_cmd->add_flag("--no-autorotate", no_autorotate, "Disable EXIF orientation"); resize_cmd->add_flag("--no-strip", no_strip, "Keep metadata on output"); resize_cmd->add_flag("--allow-enlargement", allow_enlargement, "Allow upscaling (inside/contain/outside)"); bool resize_no_cache = false; std::string resize_cache_dir; int url_timeout_sec = 5; int url_max_redirects = 20; resize_cmd->add_flag("--no-cache", resize_no_cache, "Disable output cache (default: cache on)"); resize_cmd->add_option("--cache-dir", resize_cache_dir, "Cache root (default: /cache/images)"); resize_cmd->add_option("--url-timeout", url_timeout_sec, "HTTP(S) fetch timeout (seconds, 0 = libcurl default)") ->default_val(5); resize_cmd ->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: native dialog for paths/options; optional --src/--dst seed the dialog"); #endif std::string host = "127.0.0.1"; int port = 8080; bool serve_no_cache = false; std::string serve_cache_dir; auto *serve_cmd = app.add_subcommand("serve", "Run HTTP REST server"); serve_cmd->add_option("--host", host, "Bind address")->default_val("127.0.0.1"); serve_cmd->add_option("-p,--port", port, "TCP port")->default_val(8080); serve_cmd->add_flag("--no-cache", serve_no_cache, "Server default: cache off when JSON omits \"cache\" (JSON may still enable per request)"); serve_cmd->add_option("--cache-dir", serve_cache_dir, "Server default cache dir when JSON omits \"cache_dir\""); std::string ipc_host = "127.0.0.1"; int ipc_port = 9333; std::string ipc_unix; bool ipc_no_cache = false; std::string ipc_cache_dir; auto *ipc_cmd = app.add_subcommand("ipc", "Run JSON-line IPC server (TCP; Unix socket on non-Windows)"); ipc_cmd->add_option("--host", ipc_host, "Bind address")->default_val("127.0.0.1"); ipc_cmd->add_option("-p,--port", ipc_port, "TCP port")->default_val(9333); ipc_cmd->add_option("--unix", ipc_unix, "Unix domain socket path (not Windows)"); ipc_cmd->add_flag("--no-cache", ipc_no_cache, "Server default: cache off when JSON omits \"cache\" (JSON may still enable per request)"); ipc_cmd->add_option("--cache-dir", ipc_cache_dir, "Server default cache dir when JSON omits \"cache_dir\""); 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_explorer_ui_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: \\scripts\\explorer-resize.ps1)"); reg_cmd->add_option("--explorer-convert-script", reg_explorer_convert_script, "Path to explorer-convert.ps1 (default: \\scripts\\explorer-convert.ps1)"); reg_cmd->add_option("--explorer-ui-script", reg_explorer_ui_script, "Path to explorer-ui.ps1 (default: \\scripts\\explorer-ui.ps1)"); reg_cmd->add_option("--widths", reg_widths)->default_val("1980,1200,800,400"); CLI11_PARSE(app, argc, argv); #if defined(_WIN32) bool resize_ui_mode = false; #endif if (resize_cmd->parsed()) { #if defined(_WIN32) struct UiSingletonOwner { bool mutex = false; bool bridge = false; ~UiSingletonOwner() { if (bridge) media::win::destroy_ui_singleton_bridge(); if (mutex) media::win::release_ui_singleton_mutex(); } } ui_singleton; #endif media::ResizeOptions opt; std::string in_path_e; std::string out_path_e; #if defined(_WIN32) if (resize_ui) { if (!src_list.empty() && !in_path.empty()) { std::cerr << "resize: use either positional input or --src, not both\n"; return 1; } { const std::string forward_seed = src_list.empty() ? in_path : join_src_semicolons(src_list); if (!media::win::try_acquire_ui_singleton_mutex()) { if (!forward_seed.empty()) { if (!media::win::forward_resize_ui_paths_to_primary(forward_seed)) std::cerr << "media-img: could not reach the running resize UI (try again in a moment)\n"; } else { std::cerr << "media-img: resize UI is already running\n"; } return 0; } ui_singleton.mutex = true; if (!media::win::create_ui_singleton_bridge()) { std::cerr << "media-img: could not create UI bridge window\n"; return 1; } ui_singleton.bridge = true; } resize_ui_mode = true; 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 = src_list.empty() ? in_path : join_src_semicolons(src_list); std::string ui_out = dst_flag.empty() ? out_path : dst_flag; 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_list.empty() || !dst_flag.empty(); if (use_src_dst) { if (src_list.empty() || dst_flag.empty()) { std::cerr << "resize: --src (one or more) and --dst must be used together\n"; return 1; } in_path_e = join_src_semicolons(src_list); 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 defined(_WIN32) if (resize_ui_mode) { std::string xerr; const std::string expanded_in = media::expand_resize_ui_inputs(in_path_e, xerr); if (expanded_in.empty()) { std::cerr << xerr << "\n"; return 1; } in_path_e = expanded_in; } #endif if (out_path_e.empty()) { std::string derr; out_path_e = media::default_output_path_for_resize(in_path_e, opt.format, derr, opt.output_stem_suffix); if (out_path_e.empty()) { std::cerr << derr << "\n"; return 1; } } std::string err; media::ResizeBatchResult batch; #if defined(_WIN32) bool batch_ok; { std::string preview_err; auto jobs_preview = media::pair_resize_paths(in_path_e, out_path_e, preview_err); if (!preview_err.empty()) { std::cerr << preview_err << "\n"; return 1; } const bool use_progress = resize_ui_mode || jobs_preview.size() > 1; if (use_progress) batch_ok = media::win::run_resize_batch_with_progress_ui(in_path_e, out_path_e, opt, err, &batch, resize_ui_mode); else batch_ok = media::resize_batch(in_path_e, out_path_e, opt, err, &batch); } #else const bool batch_ok = media::resize_batch(in_path_e, out_path_e, opt, err, &batch); #endif if (!batch_ok) { std::cerr << err << "\n"; return 1; } if (batch.count > 1) std::cout << batch.count << " file(s) written\n"; return 0; } if (serve_cmd->parsed()) { media::CacheServerDefaults cd; cd.enabled = !serve_no_cache; cd.cache_dir = serve_cache_dir; return media::http::run_server(host, port, cd); } if (ipc_cmd->parsed()) { media::CacheServerDefaults cd; cd.enabled = !ipc_no_cache; cd.cache_dir = ipc_cache_dir; #if defined(_WIN32) if (!ipc_unix.empty()) { std::cerr << "media-img: --unix is not supported on Windows; use --host and --port.\n"; return 1; } return media::ipc::run_tcp_server(ipc_host, ipc_port, cd); #else if (!ipc_unix.empty()) { return media::ipc::run_unix_server(ipc_unix, cd); } return media::ipc::run_tcp_server(ipc_host, ipc_port, cd); #endif } if (kbot_cmd->parsed()) { 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.explorer_ui_script = reg_explorer_ui_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; }