CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
source_manager.cpp
Go to the documentation of this file.
1
12
13#include <OpenImageIO/imagebufalgo.h>
14#include <OpenImageIO/imageio.h>
15#include <spdlog/spdlog.h>
16#include <mutex>
17#include <algorithm>
18
20
21// Global static members for cache management
22static std::shared_ptr<OIIO::ImageCache> s_global_cache_ptr { nullptr };
23static std::once_flag s_cache_init_flag;
24
32[[nodiscard]] static bool hasExtension(std::string_view path, std::span<const std::string_view> extensions) noexcept
33{
34 std::string lower_path(path);
35 std::transform(lower_path.begin(), lower_path.end(), lower_path.begin(),
36 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
37
38 for (const auto& ext : extensions)
39 {
40 if (lower_path.size() >= ext.size() &&
41 lower_path.compare(lower_path.size() - ext.size(), ext.size(), ext) == 0) {
42 return true;
43 }
44 }
45 return false;
46}
47
48OIIO::ImageCache* SourceManager::getGlobalCache()
49{
50 std::call_once(s_cache_init_flag, []() {
51 s_global_cache_ptr = OIIO::ImageCache::create();
52
54 s_global_cache_ptr->attribute("max_memory_MB", 2048.0f);
55 spdlog::debug("[SourceManager::getGlobalCache]: OIIO ImageCache created: 2GB limit");
56 } else {
57 spdlog::critical("[SourceManager::getGlobalCache]: Failed to create OIIO ImageCache");
58 }
59 });
60 return s_global_cache_ptr.get();
61}
62
64 : m_cache(getGlobalCache())
65{
66 if (!m_cache) {
67 spdlog::critical("[SourceManager::SourceManager]: Failed to get global ImageCache");
68 throw std::runtime_error("SourceManager: Initialization failed");
69 }
70}
71
73{
74 unloadInternal();
75}
76
77std::expected<void, ErrorHandling::CoreError> SourceManager::loadFile(std::string_view path)
78{
79 if (path.empty()) {
80 spdlog::warn("[SourceManager::loadFile]: Empty file path");
81 return std::unexpected(ErrorHandling::CoreError::FileNotFound);
82 }
83
84 std::lock_guard<std::mutex> lock(m_mutex);
85
86 if (isLoaded_unsafe()) {
87 unloadInternal();
88 }
89
90 spdlog::info("[SourceManager::loadFile]: Loading: '{}'", path);
91
92 // Dispatch based on file type
93 if (isRawFile(path)) {
94 return loadRawFile(path);
95 } else if (isHeicFile(path)) {
96 return loadHeicFile(path);
97 }
98
99 return loadStandardFile(path);
100}
101
102bool SourceManager::isRawFile(std::string_view path) const
103{
104 static const std::array<std::string_view, 25> raw_extensions = {
105 ".nef", ".cr2", ".cr3", ".arw", ".dng", ".raw", ".rw2",
106 ".orf", ".sr2", ".raf", ".mrw", ".pef", ".srw", ".x3f",
107 ".3fr", ".iiq", ".kdc", ".mef", ".nrw", ".srw", ".crw",
108 ".mos", ".erf", ".dc2", ".dcr"
109 };
110
111 return hasExtension(path, raw_extensions);
112}
113
114bool SourceManager::isHeicFile(std::string_view path) const
115{
116 static const std::array<std::string_view, 3> heic_extensions = {
117 ".heic", ".heif", ".avif"
118 };
119
120 return hasExtension(path, heic_extensions);
121}
122
123std::expected<void, ErrorHandling::CoreError> SourceManager::loadRawFile(std::string_view path)
124{
125 spdlog::debug("[SourceManager::loadRawFile]: Processing RAW file: {}", path);
126
127 // ============================================================
128 // Configure OIIO/LibRaw with optimized settings
129 // ============================================================
130 ImageConfig::Raw::RawSettings settings;
131 settings.set_demosaic(ImageConfig::Raw::DemosaicAlgorithm::amaze);
132 settings.set_color_space(ImageConfig::Raw::RawColorSpace::prophoto_linear);
133 settings.set_highlight_mode(ImageConfig::Raw::HighlightMode::blend);
134 settings.set_balance_clamped(true);
135 settings.set_use_camera_wb(true);
136 settings.set_fbdd_noiserd(ImageConfig::Raw::FbddNoiseRd::light);
137 settings.set_camera_matrix(ImageConfig::Raw::CameraMatrixMode::always);
138
139 OIIO::ImageSpec config;
140 config.attribute("raw:Demosaic", settings.get_demosaic_string());
141 config.attribute("raw:ColorSpace", settings.get_color_space_string());
142 config.attribute("raw:HighlightMode", settings.get_highlight_mode_value());
143 config.attribute("raw:balance_clamped", static_cast<int>(settings.get_balance_clamped()));
144 config.attribute("raw:use_camera_matrix", settings.get_camera_matrix_value());
145 config.attribute("raw:use_camera_wb", static_cast<int>(settings.get_use_camera_wb()));
146 config.attribute("raw:use_auto_wb", static_cast<int>(settings.get_use_auto_wb()));
147 config.attribute("raw:fbdd_noiserd", settings.get_fbdd_noiserd_value());
148
149 // ============================================================
150 // Load via OIIO ImageCache
151 // ============================================================
152 auto buf_result { loadImageBuffer(path, &config) };
153 if (!buf_result) {
154 spdlog::error("[SourceManager::loadRawFile]: Failed to load RAW file '{}': {}", path, buf_result.error());
155 return std::unexpected(buf_result.error());
156 }
157
158 // ============================================================
159 // Convert and store
160 // ============================================================
161 m_current_path = path;
162 auto result { convertToRgbaInternal(std::move(buf_result.value())) };
163
164 if (result) {
165 spdlog::info("[SourceManager::loadRawFile]: Loaded '{}': {}x{} RGBA_F32",
166 path, m_image_buf->spec().width, m_image_buf->spec().height);
167 }
168
169 return result;
170}
171
172std::expected<void, ErrorHandling::CoreError> SourceManager::loadHeicFile(std::string_view path)
173{
174 spdlog::debug("[SourceManager::loadHeicFile]: Processing HEIC file: {}", path);
175
176 // ============================================================
177 // Configure OIIO/libheif with HEIC settings
178 // ============================================================
179 ImageConfig::Heic::HeicSettings settings;
180
181 OIIO::ImageSpec config;
182 config.attribute("oiio:reorient", settings.get_reorient_value());
183 config.attribute("oiio:UnassociatedAlpha", settings.get_unassociated_alpha_value());
184
185 // ============================================================
186 // Load via OIIO ImageCache
187 // ============================================================
188 auto buf_result { loadImageBuffer(path, &config) };
189 if (!buf_result) {
190 spdlog::error("[SourceManager::loadHeicFile]: Failed to load HEIC file '{}': {}", path, buf_result.error());
191 return std::unexpected(buf_result.error());
192 }
193
194 // ============================================================
195 // Convert to linear (HEIC from smartphones are typically sRGB)
196 // ============================================================
197 auto [is_linear, cs_name] { Utils::analyzeColorSpace(buf_result->spec()) };
198
199 if (!is_linear && !cs_name.empty())
200 {
201 auto conversion { Utils::transformToColorSpace(*buf_result, "lin_rec709_scene") };
202 if (!conversion) {
203 spdlog::error("[SourceManager::loadHeicFile]: Color space conversion failed for '{}': {}", path, conversion.error());
204 return std::unexpected(conversion.error());
205 }
206
207 }
208 // ============================================================
209 // Convert and store
210 // ============================================================
211 m_current_path = path;
212 auto result { convertToRgbaInternal(std::move(buf_result.value())) };
213
214 if (result) {
215 spdlog::info("[SourceManager::loadHeicFile]: Loaded '{}': {}x{} RGBA_F32",
216 path, m_image_buf->spec().width, m_image_buf->spec().height);
217 }
218
219 return result;
220}
221
222std::expected<void, ErrorHandling::CoreError> SourceManager::loadStandardFile(std::string_view path)
223{
224 spdlog::debug("[SourceManager::loadStandardFile]: Processing standard image: {}", path);
225
226 // ============================================================
227 // Load via OIIO ImageCache (no special config)
228 // ============================================================
229 auto buf_result { loadImageBuffer(path, nullptr) };
230 if (!buf_result) {
231 spdlog::error("[SourceManager::loadStandardFile]: Failed to load standard file '{}': {}", path, buf_result.error());
232 return std::unexpected(buf_result.error());
233 }
234
235 // ============================================================
236 // Convert to linear if needed
237 // ============================================================
238 auto [is_linear, cs_name] { Utils::analyzeColorSpace(buf_result->spec()) };
239
240 if (!is_linear && !cs_name.empty())
241 {
242 auto conversion { Utils::transformToColorSpace(*buf_result, "lin_rec709_scene") };
243 if (!conversion) {
244 spdlog::error("[SourceManager::loadStandardFile]: Color space conversion failed for '{}': {}", path, conversion.error());
245 return std::unexpected(conversion.error());
246 }
247
248 auto [is_linear, cs_name] { Utils::analyzeColorSpace(buf_result->spec()) };
249
250 spdlog::debug("[SourceManager::loadStandardFile]: is linear: '{}' new space: '{}' ",
251 is_linear, cs_name);
252 }
253
254 // ============================================================
255 // Convert and store
256 // ============================================================
257 m_current_path = path;
258 auto result { convertToRgbaInternal(std::move(buf_result.value())) };
259
260 if (result) {
261 spdlog::info("[SourceManager::loadStandardFile]: Loaded '{}': {}x{} RGBA_F32",
262 path, m_image_buf->spec().width, m_image_buf->spec().height);
263 }
264
265 return result;
266}
267
268std::expected<OIIO::ImageBuf, ErrorHandling::CoreError>
269SourceManager::loadImageBuffer(std::string_view path, const OIIO::ImageSpec* config)
270{
271 if (config)
272 {
273 // Config path: ImageInput::open applies attributes (RAW, HEIF)
274 auto in { OIIO::ImageInput::open(std::string(path), config) };
275 if (!in) {
276 spdlog::error("[SourceManager::loadImageBuffer]: Failed to open '{}': {}", path, OIIO::geterror());
277 return std::unexpected(ErrorHandling::CoreError::DecodingError);
278 }
279
280 OIIO::ImageSpec float_spec { in->spec() };
281 float_spec.set_format(OIIO::TypeDesc::FLOAT);
282
283 OIIO::ImageBuf buf(float_spec);
284
285 if (!in->read_image(0, 0, 0, -1, OIIO::TypeDesc::FLOAT, buf.localpixels())) {
286 spdlog::error("[SourceManager::loadImageBuffer]: Failed to read '{}': {}", path, in->geterror());
287 return std::unexpected(ErrorHandling::CoreError::DecodingError);
288 }
289
290 in->close();
291 return buf;
292 }
293
294 // Standard path: ImageCache
295 OIIO::ImageBuf buf(std::string(path), 0, 0, s_global_cache_ptr);
296 if (!buf.read(0, 0, true, OIIO::TypeDesc::FLOAT)) {
297 spdlog::error("[SourceManager::loadImageBuffer]: Failed to read '{}': {}", path, buf.geterror());
298 return std::unexpected(ErrorHandling::CoreError::DecodingError);
299 }
300
301 return buf;
302}
303
304std::expected<void, ErrorHandling::CoreError> SourceManager::convertToRgbaInternal(OIIO::ImageBuf&& src_buf)
305{
306 // ============================================================
307 // Get dimensions
308 // ============================================================
309 const Common::ImageDim w { static_cast<Common::ImageDim>(src_buf.spec().width) };
310 const Common::ImageDim h { static_cast<Common::ImageDim>(src_buf.spec().height) };
311 const Common::ImageChan src_ch { static_cast<Common::ImageChan>(src_buf.spec().nchannels) };
312
313 spdlog::debug("[SourceManager::convertToRgbaInternal]: {}x{} ({} channels)", w, h, src_ch);
314
315 // ============================================================
316 // Convert to RGBA_F32
317 // ============================================================
318 OIIO::ImageSpec target_spec(static_cast<int>(w), static_cast<int>(h), 4, OIIO::TypeDesc::FLOAT);
319 OIIO::ImageBuf rgba_buf(target_spec);
320
321 std::vector<int> channel_order = {0, 1, 2, 3};
322 std::vector<float> channel_values = {0.0f, 0.0f, 0.0f, 1.0f};
323
324 switch (src_ch)
325 {
326 case 1: channel_order = {0, 0, 0, -1}; break;
327 case 2: channel_order = {0, 0, 0, 1}; break;
328 case 3: channel_order = {0, 1, 2, -1}; break;
329 default: channel_order = {0, 1, 2, 3}; break;
330 }
331
332 if (!OIIO::ImageBufAlgo::channels(rgba_buf, src_buf, 4, channel_order, channel_values)) {
333 spdlog::error("[SourceManager::convertToRgbaInternal]: Channel conversion failed: {}", rgba_buf.geterror());
334 return std::unexpected(ErrorHandling::CoreError::AllocationFailed);
335 }
336
337 // ============================================================
338 // Store final buffer
339 // ============================================================
340 m_image_buf = std::make_unique<OIIO::ImageBuf>(std::move(rgba_buf));
341
342 return {};
343}
344
346{
347 std::lock_guard<std::mutex> lock(m_mutex);
348 unloadInternal();
349}
350
351void SourceManager::unloadInternal()
352{
353 if (m_image_buf && m_image_buf->initialized()) {
354 spdlog::debug("[SourceManager::unloadInternal]: Unloading '{}'", m_current_path);
355 }
356 m_image_buf.reset();
357 m_current_path.clear();
358}
359
361{
362 std::lock_guard<std::mutex> lock(m_mutex);
363 return isLoaded_unsafe();
364}
365
366bool SourceManager::isLoaded_unsafe() const
367{
368 return m_image_buf && m_image_buf->initialized();
369}
370
372{
373 return m_image_buf ? static_cast<Common::ImageDim>(m_image_buf->spec().width) : 0;
374}
375
377{
378 return m_image_buf ? static_cast<Common::ImageDim>(m_image_buf->spec().height) : 0;
379}
380
382{
383 return m_image_buf ? static_cast<Common::ImageChan>(m_image_buf->spec().nchannels) : 0;
384}
385
386std::expected<std::unique_ptr<Common::ImageRegion>, ErrorHandling::CoreError>
389{
390 std::lock_guard<std::mutex> lock(m_mutex);
391
392 if (!isLoaded_unsafe()) {
393 spdlog::warn("[SourceManager::getTile] No image loaded");
394 return std::unexpected(ErrorHandling::CoreError::SourceNotLoaded);
395 }
396
397 if (width == 0 || height == 0) {
398 return std::unexpected(ErrorHandling::CoreError::InvalidImageRegion);
399 }
400
401 // Bounds contract: reject requests sharing NO pixel with the image.
402 // ImageDim is UNSIGNED and zero sizes are already rejected above, so a
403 // request can only miss the image on the right/bottom side: these two
404 // comparisons fully express "no intersection".
405 const auto img_width { static_cast<Common::ImageDim>(m_image_buf->spec().width) };
406 const auto img_height { static_cast<Common::ImageDim>(m_image_buf->spec().height) };
407
408 if (x >= img_width || y >= img_height) {
409 return std::unexpected(ErrorHandling::CoreError::InvalidImageRegion);
410 }
411
412 // OIIO handles partial intersections via ROI - no manual clamping needed
413 OIIO::ROI roi(
414 static_cast<int>(x), static_cast<int>(x + width),
415 static_cast<int>(y), static_cast<int>(y + height),
416 0, 1,
417 0, 4
418 );
419
420 // Allocate result (zero-initialized: the outside-intersection area
421 // of clamped requests is filled by this, not by OIIO)
422 const size_t data_size { width * height * 4 };
423 std::vector<float> data(data_size);
424
425 if (!m_image_buf->get_pixels(roi, OIIO::TypeDesc::FLOAT, data.data())) {
426 spdlog::warn("[SourceManager::getTile]: get_pixels failed: {}", m_image_buf->geterror());
427 return std::unexpected(ErrorHandling::CoreError::IOError);
428 }
429
430 // Create ImageRegion
431 auto region { std::make_unique<Common::ImageRegion>(
432 std::move(data),
433 width,
434 height,
435 static_cast<Common::ImageChan>(4)
436 )};
437
438 region->m_x = static_cast<Common::ImageCoord>(x);
439 region->m_y = static_cast<Common::ImageCoord>(y);
440
441 return region;
442}
443
444std::expected<void, ErrorHandling::CoreError>
446{
447 std::lock_guard<std::mutex> lock(m_mutex);
448
449 if (!isLoaded_unsafe()) {
450 spdlog::warn("[SourceManager::setTile] No image loaded");
451 return std::unexpected(ErrorHandling::CoreError::SourceNotLoaded);
452 }
453
454 if (!tile.isValid()) {
455 return std::unexpected(ErrorHandling::CoreError::InvalidImageRegion);
456 }
457
458 if (tile.m_format != Common::PixelFormat::RGBA_F32 || tile.m_channels != 4) {
459 spdlog::error("[SourceManager::setTile] Invalid format: expected RGBA_F32");
460 return std::unexpected(ErrorHandling::CoreError::InvalidImageRegion);
461 }
462
463 // Bounds contract: reject regions sharing NO pixel with the image.
464 // ImageRegion mixes SIGNED coordinates (ImageCoord = int32, negative
465 // offsets allowed) with UNSIGNED dimensions (ImageDim = size_t): raw
466 // arithmetic like `m_x + m_width` silently converts the negative side
467 // to size_t (e.g. -3 becomes ~1.8e19) and the check never fires.
468 // Everything is normalized to int, where the math keeps its ordinary
469 // meaning; the same locals feed the OIIO ROI (int fields).
470 const int img_width { m_image_buf->spec().width };
471 const int img_height { m_image_buf->spec().height };
472 const int tile_x { tile.m_x };
473 const int tile_y { tile.m_y };
474 const int tile_w { static_cast<int>(tile.m_width) };
475 const int tile_h { static_cast<int>(tile.m_height) };
476
477 const bool is_no_intersection {
478 tile_x >= img_width
479 || tile_y >= img_height
480 || tile_x + tile_w <= 0
481 || tile_y + tile_h <= 0
482 };
483 if (is_no_intersection) {
484 return std::unexpected(ErrorHandling::CoreError::InvalidImageRegion);
485 }
486
487 // OIIO handles partial intersections via ROI - no manual clamping needed
488 const OIIO::ROI roi(tile_x, tile_x + tile_w, tile_y, tile_y + tile_h, 0, 1, 0, 4);
489
490 if (!m_image_buf->set_pixels(roi, OIIO::TypeDesc::FLOAT, tile.m_data.data())) {
491 spdlog::error("[SourceManager::setTile] set_pixels failed: {}", m_image_buf->geterror());
492 return std::unexpected(ErrorHandling::CoreError::IOError);
493 }
494
495 return {};
496}
497
498std::optional<std::string> SourceManager::getMetadata(std::string_view key) const
499{
500 std::lock_guard<std::mutex> lock(m_mutex);
501
502 if (!m_image_buf) {
503 return std::nullopt;
504 }
505
506 const auto& spec { m_image_buf->spec() };
507 const auto* attr { spec.find_attribute(std::string(key)) };
508
509 return attr ? std::optional{attr->get_string()} : std::nullopt;
510}
511
513{
514 std::lock_guard lock(m_mutex);
515 return m_current_path;
516}
517
518} // namespace CaptureMoment::Core::Managers
std::expected< std::unique_ptr< Common::ImageRegion >, ErrorHandling::CoreError > getTile(Common::ImageDim x, Common::ImageDim y, Common::ImageDim width, Common::ImageDim height) override
Extracts a rectangular region (tile) of pixels from the image.
Common::ImageDim width() const noexcept override
Retrieves the width (in pixels) of the source image.
bool isLoaded() const override
Checks if an image is currently loaded.
Common::ImageDim height() const noexcept override
Retrieves the height (in pixels) of the source image.
std::expected< void, ErrorHandling::CoreError > setTile(const Common::ImageRegion &tile) override
Writes pixel data from a tile back into the image buffer.
void unload() override
Unloads the currently loaded image and frees resources.
std::string getImageSourcePath() const override
Gets the path of the image source.
std::expected< void, ErrorHandling::CoreError > loadFile(std::string_view path) override
Loads an image file from the specified path.
Common::ImageChan channels() const noexcept override
Gets the number of channels of the internal buffer.
std::optional< std::string > getMetadata(std::string_view key) const override
Retrieves a specific metadata field from the source image.
Utilities for analyzing and converting image color spaces.
Input configuration settings for HEIF/HEIC/AVIF image loading via OIIO/libheif.
std::uint8_t ImageChan
Type for channel count.
Definition image_types.h:26
std::int32_t ImageCoord
Type for pixel coordinates (x, y).
Definition image_types.h:32
@ RGBA_F32
4-channel format: Red, Green, Blue, Alpha in 32-bit float.
std::size_t ImageDim
Type for image dimensions (width, height).
Definition image_types.h:20
CoreError
Enumeration of all error codes for CaptureMoment Core.
Definition core_error.h:36
@ InvalidImageRegion
The provided ImageRegion dimensions or data are invalid.
@ FileNotFound
The specified file path does not exist.
@ DecodingError
Failed to decode the image data.
@ SourceNotLoaded
Attempted to access image data without loading a file first.
@ AllocationFailed
Memory allocation failed.
@ always
Always use embedded color matrix when available (recommended).
@ prophoto_linear
ProPhoto primaries with linear transfer. Best for editing (recommended).
@ blend
Blend clipped areas with surrounding unclipped data. Natural-looking highlights.
@ amaze
Adaptive Malvar-He-Cutler algorithm. Best quality, slower.
@ light
Light FBDD reduction (recommended for most images).
static std::once_flag s_cache_init_flag
static bool hasExtension(std::string_view path, std::span< const std::string_view > extensions) noexcept
Checks if a file path has one of the specified extensions.
static std::shared_ptr< OIIO::ImageCache > s_global_cache_ptr
ColorSpaceInfo analyzeColorSpace(const OIIO::ImageSpec &spec) noexcept
Analyzes whether an image's pixel data is stored in a linear color space.
std::expected< void, ErrorHandling::CoreError > transformToColorSpace(OIIO::ImageBuf &image, std::string_view target_cs)
Converts an image buffer to a target color space (in-place).
Comprehensive RAW development settings with runtime configuration.
Image source management class using OpenImageIO (OIIO).
Represents a rectangular region of an image and its pixel data.
ImageDim m_width
Width of this image region in pixels.
ImageDim m_height
Height of this image region in pixels.
std::vector< float > m_data
Pixel data (row-major layout).
ImageCoord m_x
X-coordinate of the top-left corner of this region in the full source image.
ImageCoord m_y
Y-coordinate of the top-left corner of this region in the full source image.
PixelFormat m_format
Format specifying how pixels are stored.
constexpr bool isValid() const noexcept
Validates the integrity of the ImageRegion (Overflow-Safe Version).
ImageChan m_channels
Number of color channels per pixel.
CaptureMoment — footer