Before decoding, loadImages reads every header to size the batch against the free memory. That pass was serial, silent and could not be cancelled, so the loading window sat at "0 of N". LoadProgress gains onSized, the headers are read in parallel on the same cursor-driven workers as the decodes, and a cancel stops the pass. The window's bar runs twice, "Checking sizes" then "Decoding", and the CLI prints a sizing line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.8 KiB
C++
91 lines
3.8 KiB
C++
#pragma once
|
|
|
|
#include <atomic>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace effector {
|
|
|
|
// Tightly packed 8-bit RGB. Row stride is always width * 3, which lets the
|
|
// effects work with plain memcpy and hands GDK a buffer it can upload as is.
|
|
class Image {
|
|
public:
|
|
Image() = default;
|
|
Image(int width, int height);
|
|
|
|
int width() const noexcept { return width_; }
|
|
int height() const noexcept { return height_; }
|
|
std::size_t stride() const noexcept { return static_cast<std::size_t>(width_) * 3; }
|
|
bool empty() const noexcept { return width_ <= 0 || height_ <= 0; }
|
|
|
|
std::uint8_t* row(int y) noexcept { return pixels_.data() + static_cast<std::size_t>(y) * stride(); }
|
|
const std::uint8_t* row(int y) const noexcept {
|
|
return pixels_.data() + static_cast<std::size_t>(y) * stride();
|
|
}
|
|
std::uint8_t* data() noexcept { return pixels_.data(); }
|
|
const std::uint8_t* data() const noexcept { return pixels_.data(); }
|
|
std::size_t byteSize() const noexcept { return pixels_.size(); }
|
|
|
|
const std::string& path() const noexcept { return path_; }
|
|
void setPath(std::string path) { path_ = std::move(path); }
|
|
|
|
// Decodes via GdkPixbuf (PNG, JPEG, WebP, TIFF, BMP, ...), applying the
|
|
// embedded EXIF orientation. Throws std::runtime_error on failure.
|
|
// Transparency is flattened onto white, as a viewer would show it, unless
|
|
// `alphaOut` is given: it then receives one byte per pixel (255 throughout
|
|
// for a file without an alpha channel) and the colours are left untouched,
|
|
// which is what an effect compositing the image needs.
|
|
static Image load(const std::string& path, std::vector<std::uint8_t>* alphaOut = nullptr);
|
|
|
|
// A copy shrunk so that its longer side is at most `maxSide`, each pixel
|
|
// the average of the ones it covers; an image already that small comes
|
|
// back as it is. For previews: nothing rendered goes through it.
|
|
Image scaledToFit(int maxSide) const;
|
|
|
|
// Encodes by file extension; png is used when the extension is unknown.
|
|
void save(const std::string& path) const;
|
|
|
|
private:
|
|
int width_ = 0;
|
|
int height_ = 0;
|
|
std::vector<std::uint8_t> pixels_;
|
|
std::string path_;
|
|
};
|
|
|
|
struct LoadFailure {
|
|
std::string path;
|
|
std::string message;
|
|
};
|
|
|
|
// Lets a caller follow a long batch decode and abort it. Both members are
|
|
// touched from the decoding threads, so `onSized` and `onFile` must be
|
|
// thread-safe and must not block: the GUI only bumps a counter from them and
|
|
// wakes the main thread. `onSized` follows the first pass, which reads every
|
|
// header to size the batch; `onFile` follows the decodes after it.
|
|
struct LoadProgress {
|
|
std::function<void(std::size_t done, std::size_t total, const std::string& path)> onSized;
|
|
std::function<void(std::size_t done, std::size_t total, const std::string& path)> onFile;
|
|
std::atomic<bool>* cancel = nullptr;
|
|
};
|
|
|
|
// The pixel count of an image file from its header alone, without decoding
|
|
// it; 0 when the header cannot be read.
|
|
std::size_t imageFilePixels(const std::string& path);
|
|
|
|
// Loads every path, collecting rather than throwing on individual failures.
|
|
// When `progress->cancel` is set the remaining paths are skipped, so the result
|
|
// is short and should be discarded rather than used.
|
|
//
|
|
// Before decoding anything the whole batch is sized from the file headers,
|
|
// and a batch that would not fit in the free memory throws OutOfMemoryError
|
|
// (core/memory.hpp) with nothing loaded; the decodes that run at once are
|
|
// also held to what is left over.
|
|
std::vector<Image> loadImages(const std::vector<std::string>& paths,
|
|
std::vector<LoadFailure>& failures,
|
|
const LoadProgress* progress = nullptr);
|
|
|
|
} // namespace effector
|