#include "core/image.hpp" #include #include #include #include #include #include #include #include #include namespace effector { namespace { // The plain C pixbuf API is used on purpose: it needs no gtkmm wrapper // registration and no display, so the core renders headlessly. using PixbufPtr = std::unique_ptr; PixbufPtr wrap(GdkPixbuf* pixbuf) { return PixbufPtr(pixbuf, &g_object_unref); } std::string takeError(GError* error) { std::string message = error != nullptr && error->message != nullptr ? error->message : "unknown error"; if (error != nullptr) g_error_free(error); return message; } std::string lowerExtension(const std::string& path) { const auto dot = path.find_last_of('.'); if (dot == std::string::npos) return {}; std::string ext = path.substr(dot + 1); std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return ext; } } // namespace Image::Image(int width, int height) : width_(width), height_(height), pixels_(static_cast(std::max(width, 0)) * 3 * static_cast(std::max(height, 0))) {} Image Image::load(const std::string& path, std::vector* alphaOut) { GError* error = nullptr; PixbufPtr pixbuf = wrap(gdk_pixbuf_new_from_file(path.c_str(), &error)); if (!pixbuf) throw std::runtime_error(takeError(error)); if (GdkPixbuf* oriented = gdk_pixbuf_apply_embedded_orientation(pixbuf.get())) { pixbuf = wrap(oriented); } const int width = gdk_pixbuf_get_width(pixbuf.get()); const int height = gdk_pixbuf_get_height(pixbuf.get()); const int channels = gdk_pixbuf_get_n_channels(pixbuf.get()); const int srcStride = gdk_pixbuf_get_rowstride(pixbuf.get()); const guint8* src = gdk_pixbuf_get_pixels(pixbuf.get()); Image image(width, height); if (alphaOut != nullptr) { alphaOut->assign(static_cast(std::max(width, 0)) * static_cast(std::max(height, 0)), 255); } for (int y = 0; y < height; ++y) { const guint8* in = src + static_cast(y) * srcStride; std::uint8_t* out = image.row(y); if (channels == 3) { std::memcpy(out, in, static_cast(width) * 3); } else if (alphaOut != nullptr) { // Kept apart rather than flattened: the caller does the compositing. std::uint8_t* alpha = alphaOut->data() + static_cast(y) * width; for (int x = 0; x < width; ++x) { for (int c = 0; c < 3; ++c) out[x * 3 + c] = in[x * channels + c]; alpha[x] = in[x * channels + 3]; } } else { // Flatten alpha onto white, matching what a viewer would show. for (int x = 0; x < width; ++x) { const int a = in[x * channels + 3]; for (int c = 0; c < 3; ++c) { const int value = in[x * channels + c] * a + 255 * (255 - a); out[x * 3 + c] = static_cast((value + 127) / 255); } } } } image.setPath(path); return image; } void Image::save(const std::string& path) const { if (empty()) throw std::runtime_error("nothing to save"); PixbufPtr pixbuf = wrap(gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width_, height_)); if (!pixbuf) throw std::runtime_error("out of memory"); const int dstStride = gdk_pixbuf_get_rowstride(pixbuf.get()); guint8* dst = gdk_pixbuf_get_pixels(pixbuf.get()); for (int y = 0; y < height_; ++y) { std::memcpy(dst + static_cast(y) * dstStride, row(y), stride()); } const std::string ext = lowerExtension(path); std::string type = "png"; if (ext == "jpg" || ext == "jpeg") type = "jpeg"; else if (ext == "webp") type = "webp"; else if (ext == "tif" || ext == "tiff") type = "tiff"; else if (ext == "bmp") type = "bmp"; GError* error = nullptr; const gboolean saved = type == "jpeg" ? gdk_pixbuf_save(pixbuf.get(), path.c_str(), type.c_str(), &error, "quality", "95", nullptr) : gdk_pixbuf_save(pixbuf.get(), path.c_str(), type.c_str(), &error, nullptr); if (saved == FALSE) throw std::runtime_error(takeError(error)); } std::vector loadImages(const std::vector& paths, std::vector& failures, const LoadProgress* progress) { // Decoding is the dominant cost of a headless run, and the files are // independent, so they are decoded in parallel. Threads take the next path // from a shared cursor rather than a fixed slice, so one huge file cannot // stall the files behind it and progress arrives at a steady rate. Slots // keep the result in the order the paths were given. std::vector slots(paths.size()); std::vector errors(paths.size()); std::atomic cursor{0}; std::atomic done{0}; unsigned hardware = std::thread::hardware_concurrency(); if (hardware == 0) hardware = 1; const std::size_t workers = std::min(hardware, std::max(paths.size(), 1)); const auto decode = [&] { for (;;) { if (progress != nullptr && progress->cancel != nullptr && progress->cancel->load(std::memory_order_relaxed)) { return; } const std::size_t i = cursor.fetch_add(1, std::memory_order_relaxed); if (i >= paths.size()) return; try { slots[i] = Image::load(paths[i]); } catch (const std::exception& error) { errors[i] = error.what(); } if (progress != nullptr && progress->onFile) { progress->onFile(done.fetch_add(1, std::memory_order_relaxed) + 1, paths.size(), paths[i]); } } }; std::vector threads; threads.reserve(workers > 0 ? workers - 1 : 0); for (std::size_t w = 1; w < workers; ++w) threads.emplace_back(decode); decode(); for (auto& thread : threads) thread.join(); std::vector images; images.reserve(paths.size()); for (std::size_t i = 0; i < paths.size(); ++i) { if (!errors[i].empty()) { failures.push_back({paths[i], errors[i]}); } else if (!slots[i].empty()) { images.push_back(std::move(slots[i])); } } return images; } } // namespace effector