diff --git a/inc/cvc/image/image.h b/inc/cvc/image/image.h index d7f3810b2..ef6c8097d 100644 --- a/inc/cvc/image/image.h +++ b/inc/cvc/image/image.h @@ -139,6 +139,12 @@ void write_image(const image &img, const std::string &path, int quality = 90); // lower-cased file extension without the dot ("" if none). std::string image_file_extension(const std::string &path); +// Initialize ImageMagick, once, after pointing it at its coder modules and config. On Windows a +// cvcpkg ImageMagick is extracted rather than installed, so until then it has no coders at all, +// not even xc:. read_image/write_image call this themselves; code that uses Magick++ directly +// calls it first. A no-op in a build without ImageMagick. +void init_imagemagick(); + } // namespace cvc #endif // __CVC_IMAGE_H__ diff --git a/src/cvc-cli/cvc_cli_test.cpp b/src/cvc-cli/cvc_cli_test.cpp index 0ae179412..1ead2fc46 100644 --- a/src/cvc-cli/cvc_cli_test.cpp +++ b/src/cvc-cli/cvc_cli_test.cpp @@ -39,6 +39,13 @@ #if defined(_WIN32) #include +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include #define CVC_GETPID() _getpid() #define CVC_POPEN(cmd, mode) _popen(cmd, mode) #define CVC_PCLOSE(fp) _pclose(fp) @@ -50,9 +57,41 @@ #define CVC_POPEN(cmd, mode) popen(cmd, mode) #define CVC_PCLOSE(fp) pclose(fp) #endif +#if defined(__APPLE__) +#include +#include +#endif namespace fs = std::filesystem; +namespace { +// This test binary's own path (cvc-cli is built beside it), or empty when it cannot be found. +// /proc/self/exe exists only on Linux; elsewhere fs::canonical threw from SetUp(). +fs::path self_exe() { +#if defined(_WIN32) + std::wstring buf(32768, L'\0'); + const DWORD n = GetModuleFileNameW(nullptr, buf.data(), static_cast(buf.size())); + if (n == 0 || n >= buf.size()) + return {}; + buf.resize(n); + return fs::path(buf); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::string buf(size, '\0'); + if (_NSGetExecutablePath(buf.data(), &size) != 0) + return {}; + std::error_code ec; + fs::path p = fs::canonical(buf.c_str(), ec); + return ec ? fs::path() : p; +#else + std::error_code ec; + fs::path p = fs::canonical("/proc/self/exe", ec); + return ec ? fs::path() : p; +#endif +} +} // namespace + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -208,10 +247,13 @@ class CvcCliTest : public ::testing::Test { if (env && fs::exists(env)) { cvc_bin = env; } else { - fs::path self = fs::canonical("/proc/self/exe"); - fs::path dir = self.parent_path(); - if (fs::exists(dir / "cvc")) - cvc_bin = (dir / "cvc").string(); +#if defined(_WIN32) + const fs::path bin = self_exe().parent_path() / "cvc.exe"; +#else + const fs::path bin = self_exe().parent_path() / "cvc"; +#endif + if (!bin.parent_path().empty() && fs::exists(bin)) + cvc_bin = bin.string(); else GTEST_SKIP() << "cvc binary not found; set CVC_CLI_BINARY env var"; } diff --git a/src/cvc/image/magick_io.cpp b/src/cvc/image/magick_io.cpp index 84bbc5532..e9cbb89c8 100644 --- a/src/cvc/image/magick_io.cpp +++ b/src/cvc/image/magick_io.cpp @@ -51,27 +51,41 @@ namespace { // modules FLAT — a coder resolves its own CORE_RL_* deps in its directory, so the // modules must sit beside them — and \share\imagemagick holds the .xml // config. Only set what the environment has not already overridden (a user wins). -void set_magick_paths_win() { - HMODULE h = nullptr; - GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | - GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(&set_magick_paths_win), &h); +// The directory a loaded module's file is in; empty if it cannot be found. +std::string module_dir(HMODULE h) { if (!h) - return; + return std::string(); char buf[MAX_PATH]; const DWORD n = GetModuleFileNameA(h, buf, MAX_PATH); if (n == 0 || n >= MAX_PATH) - return; + return std::string(); std::string p(buf, n); const auto slash = p.find_last_of("\\/"); - if (slash == std::string::npos) + return slash == std::string::npos ? std::string() : p.substr(0, slash); +} + +// The module holding `addr`. +HMODULE module_at(const void *addr) { + HMODULE h = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(addr), &h); + return h; +} + +void set_magick_paths_win() { + const std::string dllDir = + module_dir(module_at(reinterpret_cast(&set_magick_paths_win))); + if (dllDir.empty()) return; - // Search order: DLL's own directory (normal case: \bin), then - // \bin from the DLL's parent (in case cvc.dll got copied to - // build\bin\Release\ for CTest while the real coders live in deps\bin), then - // any CVC_DEPS_PREFIX\bin the environment names. Pick the first that contains - // at least one IM_MOD_RL_*.dll — an empty dir here becomes a MIFF-only Magick. - const std::string dllDir = p.substr(0, slash); + // Search order: DLL's own directory (normal case: \bin); then the + // directory the ImageMagick DLL itself was loaded from, since the coders sit + // beside it (a dev build's cvc.dll is in build\bin while ImageMagick comes + // from the deps prefix on PATH); then \bin from the DLL's parent (in + // case cvc.dll got copied to build\bin\Release\ for CTest while the real + // coders live in deps\bin); then any CVC_DEPS_PREFIX\bin the environment + // names. Pick the first that contains at least one IM_MOD_RL_*.dll — an empty + // dir here becomes a Magick with no coders, not even xc: or MIFF. auto has_coder = [](const std::string &dir) -> bool { WIN32_FIND_DATAA d; HANDLE h = FindFirstFileA((dir + "\\IM_MOD_RL_*.dll").c_str(), &d); @@ -81,6 +95,14 @@ void set_magick_paths_win() { return true; }; std::vector cand{dllDir}; + // The ImageMagick DLL by its name in the cvcpkg bundle (the official Windows + // build's), then by the address of one of its functions, for other builds. + // When that address is an import thunk in cvc.dll it names dllDir again. + for (const std::string &d : + {module_dir(GetModuleHandleA("CORE_RL_MagickCore_.dll")), + module_dir(module_at(reinterpret_cast(&MagickCore::GetMagickVersion)))}) + if (!d.empty()) + cand.push_back(d); // If dllDir is \build\bin\Release, walk up to try \bin. cand.push_back(dllDir + "\\..\\..\\..\\..\\deps\\bin"); for (const char *var : {"CVC_DEPS_PREFIX", "LIBCVC_DEPS_PREFIX", "CVCPKG_PREFIX"}) @@ -262,6 +284,12 @@ class magick_image_io : public image_file_io { #endif // CVC_ENABLE_IMAGEMAGICK namespace cvc { +void init_imagemagick() { +#ifdef CVC_ENABLE_IMAGEMAGICK + init_magick_once(); +#endif +} + void register_stb_image_handler(); // stb_io.cpp — no dependencies at all. // Register the built-in image handlers. Called once (lazily) from image.cpp. diff --git a/src/cvc/tests/image_test.cpp b/src/cvc/tests/image_test.cpp index 38f173e68..8fc1bfd9c 100644 --- a/src/cvc/tests/image_test.cpp +++ b/src/cvc/tests/image_test.cpp @@ -435,7 +435,9 @@ TEST(ImageTest, ReadMissingFileThrows) { EXPECT_ANY_THROW(image::load("/no/such/ // normalization the raw C,M,Y,K quantums land in R,G,B,A — a pure-cyan CMYK pixel // came back bright RED. Synthesize a genuine CMYK file and assert read() -> cyan. TEST(ImageTest, ReadsCmykInputAsRgb) { - Magick::InitializeMagick(nullptr); // idempotent; the test uses Magick++ directly + // The test uses Magick++ directly, so it initializes Magick as libcvc does: on Windows, + // InitializeMagick alone leaves a cvcpkg ImageMagick without coders (not even xc:). + cvc::init_imagemagick(); // A 4x4 pure-cyan image transformed into the CMYK colorspace, so the file on // disk is genuinely CMYK (not an RGB image merely tagged CMYK). Magick::Image cyan(Magick::Geometry(4, 4), Magick::ColorRGB(0.0, 1.0, 1.0)); diff --git a/src/cvc/volume/volume_ops.cpp b/src/cvc/volume/volume_ops.cpp index 276e1cdf0..9e746cbe9 100644 --- a/src/cvc/volume/volume_ops.cpp +++ b/src/cvc/volume/volume_ops.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -609,6 +610,7 @@ volume vol_back_project(const volume &projections, const std::vector &an void vol_to_slices(const volume &vol, const std::string &directory, const std::string &format) { #ifdef CVC_ENABLE_IMAGEMAGICK + init_imagemagick(); // Normalise to [0,255] UChar for image output volume norm = vol_normalize(vol, 0.0, 255.0); @@ -641,6 +643,7 @@ volume slices_to_volume(app &ctx, const std::vector &paths, const b #ifdef CVC_ENABLE_IMAGEMAGICK if (paths.empty()) throw std::invalid_argument("slices_to_volume: empty path list"); + init_imagemagick(); // Read first image for dimensions Magick::Image first(paths[0]);