MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ImageReader.cpp
Go to the documentation of this file.
1#include "ImageReader.hpp"
2
4
7
9
10#ifdef MAYAFLUX_ARCH_X64
11#define STBI_SSE2
12#ifdef MAYAFLUX_COMPILER_MSVC
13#include <immintrin.h>
14#endif
15#endif
16
17#ifdef MAYAFLUX_ARCH_ARM64
18#define STBI_NEON
19#endif
20
21#define STBI_NO_FAILURE_STRINGS
22#define STB_IMAGE_IMPLEMENTATION
23#define STBI_NO_STDIO
24
25#if __has_include("stb/stb_image.h")
26#include "stb/stb_image.h"
27#elif __has_include("stb_image.h")
28#include "stb_image.h"
29#else
30#error "stb_image.h not found"
31#endif
32
33#include <tinyexr.h>
34
35#include <cstddef>
36#include <fstream>
37
38namespace MayaFlux::IO {
39
40namespace {
41
42 std::string extension_of(const std::filesystem::path& path)
43 {
44 auto ext = path.extension().string();
45 if (!ext.empty() && ext[0] == '.') {
46 ext = ext.substr(1);
47 }
48 std::ranges::transform(ext, ext.begin(),
49 [](unsigned char c) { return std::tolower(c); });
50 return ext;
51 }
52
53 /**
54 * @brief Decode an OpenEXR file from an in-memory byte buffer.
55 *
56 * Uses tinyexr's ParseEXRHeaderFromMemory + LoadEXRImageFromMemory pair to
57 * access arbitrary channel counts and names. Reinterleaves tinyexr's planar
58 * output into pixel-interleaved float layout to match ImageData's contract.
59 *
60 * Maps channel count to ImageFormat (R32F / RG32F / RGBA32F). 3-channel EXR
61 * (BGR) is promoted to 4-channel (ABGR) with A=1.0 because we don't support
62 * a 3-channel float ImageFormat.
63 *
64 * Half-precision pixel types are converted to float on the fly; EXR files
65 * with mixed pixel types are not currently supported.
66 */
67 std::optional<ImageData> load_exr_from_memory(
68 const unsigned char* bytes, size_t size)
69 {
71
72 EXRVersion version;
73 if (ParseEXRVersionFromMemory(&version, bytes, size) != TINYEXR_SUCCESS) {
75 "EXR: failed to parse version");
76 return std::nullopt;
77 }
78 if (version.multipart || version.non_image) {
80 "EXR: multipart and deep images are not supported");
81 return std::nullopt;
82 }
83
84 EXRHeader header;
85 InitEXRHeader(&header);
86
87 const char* err = nullptr;
88 if (ParseEXRHeaderFromMemory(&header, &version, bytes, size, &err)
89 != TINYEXR_SUCCESS) {
91 "EXR: header parse failed: {}", err ? err : "unknown");
92 if (err)
93 FreeEXRErrorMessage(err);
94 return std::nullopt;
95 }
96
97 for (int i = 0; i < header.num_channels; ++i) {
98 if (header.pixel_types[i] == TINYEXR_PIXELTYPE_UINT) {
100 "EXR: uint pixel type channel '{}' not supported",
101 header.channels[i].name);
102 FreeEXRHeader(&header);
103 return std::nullopt;
104 }
105 header.requested_pixel_types[i] = TINYEXR_PIXELTYPE_FLOAT;
106 }
107
108 EXRImage image;
109 InitEXRImage(&image);
110
111 if (LoadEXRImageFromMemory(&image, &header, bytes, size, &err)
112 != TINYEXR_SUCCESS) {
114 "EXR: image load failed: {}", err ? err : "unknown");
115 if (err)
116 FreeEXRErrorMessage(err);
117 FreeEXRHeader(&header);
118 return std::nullopt;
119 }
120
121 const auto width = static_cast<uint32_t>(image.width);
122 const auto height = static_cast<uint32_t>(image.height);
123 const auto src_channels = static_cast<uint32_t>(image.num_channels);
124
125 if (width == 0 || height == 0 || src_channels == 0) {
127 "EXR: zero dimensions or channels");
128 FreeEXRImage(&image);
129 FreeEXRHeader(&header);
130 return std::nullopt;
131 }
132
133 uint32_t out_channels = src_channels;
134 F format;
135 switch (src_channels) {
136 case 1:
137 format = F::R32F;
138 break;
139 case 2:
140 format = F::RG32F;
141 break;
142 case 3:
143 out_channels = 4;
144 format = F::RGBA32F;
145 break;
146 case 4:
147 format = F::RGBA32F;
148 break;
149 default:
151 "EXR: {} channels not supported (1/2/3/4 only)", src_channels);
152 FreeEXRImage(&image);
153 FreeEXRHeader(&header);
154 return std::nullopt;
155 }
156
157 const size_t pixel_count = static_cast<size_t>(width) * height;
158 const size_t element_count = pixel_count * out_channels;
159
160 ImageData result;
161 auto& dst = result.pixels.emplace<std::vector<float>>(element_count, 0.0F);
162
163 auto** planes = reinterpret_cast<float**>(image.images);
164
165 for (size_t p = 0; p < pixel_count; ++p) {
166 for (uint32_t c = 0; c < src_channels; ++c) {
167 dst[p * out_channels + c] = planes[c][p];
168 }
169 if (out_channels > src_channels) {
170 dst[p * out_channels + (out_channels - 1)] = 1.0F;
171 }
172 }
173
174 result.width = width;
175 result.height = height;
176 result.channels = out_channels;
177 result.format = format;
178
180 "Loaded EXR: {}x{}, {} channels{}",
181 width, height, out_channels,
182 (src_channels == 3) ? " [BGR→ABGR, A=1.0]" : "");
183
184 FreeEXRImage(&image);
185 FreeEXRHeader(&header);
186
187 return result;
188 }
189
190} // namespace
191
192[[maybe_unused]] static bool g_stb_simd_logged = []() {
193#ifdef STBI_SSE2
195 "STB Image: SSE2 SIMD optimizations enabled (x64)");
196#elif defined(STBI_NEON)
198 "STB Image: NEON SIMD optimizations enabled (ARM64)");
199#else
201 "STB Image: No SIMD optimizations (scalar fallback)");
202#endif
203 return true;
204}();
205
207{
209
210 const bool has_u8 = std::holds_alternative<std::vector<uint8_t>>(pixels);
211 const bool has_u16 = std::holds_alternative<std::vector<uint16_t>>(pixels);
212 const bool has_f32 = std::holds_alternative<std::vector<float>>(pixels);
213
214 switch (format) {
215 case F::R8:
216 case F::RG8:
217 case F::RGBA8:
218 case F::BGRA8:
219 return has_u8;
220
221 case F::R16:
222 case F::RG16:
223 case F::RGBA16:
224 return has_u16;
225
226 case F::R16F:
227 case F::RG16F:
228 case F::RGBA16F:
229 return has_u16;
230
231 case F::R32F:
232 case F::RG32F:
233 case F::RGBA32F:
234 return has_f32;
235
236 default:
237 return false;
238 }
239}
240
242 : m_is_open(false)
243{
244}
245
250
251bool ImageReader::can_read(const std::string& filepath) const
252{
253 auto ext = std::filesystem::path(filepath).extension().string();
254 if (!ext.empty() && ext[0] == '.') {
255 ext = ext.substr(1);
256 }
257
258 static const std::vector<std::string> supported = {
259 "png", "jpg", "jpeg", "bmp", "tga", "psd", "gif", "hdr", "pic", "pnm",
260 "exr"
261 };
262
263 return std::ranges::find(supported, ext) != supported.end();
264}
265
266bool ImageReader::open(const std::string& filepath, FileReadOptions /*options*/)
267{
268 if (m_is_open) {
269 close();
270 }
271
272 if (!can_read(filepath)) {
273 m_last_error = "Unsupported image format: " + filepath;
275 return false;
276 }
277
278 auto resolved = resolve_path(filepath);
279
280 m_image_data = load(resolved, 4); // Force RGBA
281
282 if (!m_image_data) {
283 m_last_error = "Failed to load image data";
284 return false;
285 }
286
287 m_filepath = filepath;
288 m_is_open = true;
289
291 "Opened image: {} ({}x{}, {} channels)",
292 filepath, m_image_data->width, m_image_data->height, m_image_data->channels);
293
294 return true;
295}
296
298{
299 if (m_is_open) {
300 m_image_data.reset();
301 m_filepath.clear();
302 m_is_open = false;
303 }
304}
305
307{
308 return m_is_open;
309}
310
311std::optional<FileMetadata> ImageReader::get_metadata() const
312{
313 if (!m_is_open || !m_image_data) {
314 return std::nullopt;
315 }
316
317 FileMetadata meta;
318 meta.format = "8-bit";
319
320 meta.attributes["width"] = m_image_data->width;
321 meta.attributes["height"] = m_image_data->height;
323
324 return meta;
325}
326
327std::vector<FileRegion> ImageReader::get_regions() const
328{
329 // Images don't typically have regions
330 return {};
331}
332
333std::vector<Kakshya::DataVariant> ImageReader::read_all()
334{
335 if (!m_is_open || !m_image_data) {
336 m_last_error = "No image open";
337 return {};
338 }
339
340 return std::visit(
341 [](const auto& vec) -> std::vector<Kakshya::DataVariant> {
342 return { Kakshya::DataVariant { vec } };
343 },
344 m_image_data->pixels);
345}
346
347std::vector<Kakshya::DataVariant> ImageReader::read_region(const FileRegion& region)
348{
349 if (!m_is_open || !m_image_data) {
350 m_last_error = "No image open";
351 return {};
352 }
353
354 if (region.start_coordinates.size() < 2 || region.end_coordinates.size() < 2) {
355 m_last_error = "Invalid region coordinates for image";
356 return {};
357 }
358
359 auto x_start = static_cast<uint32_t>(region.start_coordinates[0]);
360 auto y_start = static_cast<uint32_t>(region.start_coordinates[1]);
361 auto x_end = static_cast<uint32_t>(region.end_coordinates[0]);
362 auto y_end = static_cast<uint32_t>(region.end_coordinates[1]);
363
364 if (x_end > m_image_data->width || y_end > m_image_data->height) {
365 m_last_error = "Region out of bounds";
366 return {};
367 }
368
369 uint32_t region_width = x_end - x_start;
370 uint32_t region_height = y_end - y_start;
371
372 const size_t bytes_per_pixel = m_image_data->byte_size() / m_image_data->element_count() * m_image_data->channels;
373 const size_t bytes_per_elem = m_image_data->byte_size() / m_image_data->element_count();
374 const size_t pixel_stride_bytes = bytes_per_elem * m_image_data->channels;
375
376 std::vector<uint8_t> region_data(
377 static_cast<size_t>(region_width) * region_height * pixel_stride_bytes);
378
379 const auto* src = static_cast<const uint8_t*>(m_image_data->data());
380
381 for (uint32_t y = 0; y < region_height; ++y) {
382 size_t src_offset = (static_cast<size_t>((y_start + y) * m_image_data->width + x_start)) * pixel_stride_bytes;
383 size_t dst_offset = static_cast<size_t>(y * region_width) * pixel_stride_bytes;
384 size_t row_size = static_cast<size_t>(region_width) * pixel_stride_bytes;
385 std::memcpy(
386 region_data.data() + dst_offset,
387 src + src_offset,
388 row_size);
389 }
390
391 return { region_data };
392}
393
394std::shared_ptr<Kakshya::SignalSourceContainer> ImageReader::create_container()
395{
396 m_last_error = "Images use direct GPU texture creation, not containers";
397 return nullptr;
398}
399
400bool ImageReader::load_into_container(std::shared_ptr<Kakshya::SignalSourceContainer> /*container*/)
401{
402 m_last_error = "Images cannot be loaded into SignalSourceContainer";
403 return false;
404}
405
406std::vector<uint64_t> ImageReader::get_read_position() const
407{
408 return { 0, 0 };
409}
410
411bool ImageReader::seek(const std::vector<uint64_t>& /*position*/)
412{
413 return true;
414}
415
416std::vector<std::string> ImageReader::get_supported_extensions() const
417{
418 return { "png", "jpg", "jpeg", "bmp", "tga", "psd", "gif", "hdr", "pic", "pnm",
419 "exr" };
420}
421
422std::type_index ImageReader::get_data_type() const
423{
424 return typeid(std::vector<uint8_t>);
425}
426
427std::type_index ImageReader::get_container_type() const
428{
429 return typeid(void);
430}
431
433{
434 return m_last_error;
435}
436
438{
439 return false;
440}
441
443{
444 return 0;
445}
446
448{
449 return 2;
450}
451
452std::vector<uint64_t> ImageReader::get_dimension_sizes() const
453{
454 if (!m_is_open || !m_image_data) {
455 return {};
456 }
457 return { m_image_data->width, m_image_data->height };
458}
459
460//==============================================================================
461// Static Utility Methods
462//==============================================================================
463
464std::optional<ImageData> ImageReader::load(const std::filesystem::path& path, int desired_channels)
465{
466 if (!std::filesystem::exists(path)) {
468 "Image file not found: {}", path.string());
469 return std::nullopt;
470 }
471
472 std::ifstream file(path, std::ios::binary | std::ios::ate);
473 if (!file.is_open()) {
475 "Failed to open image file: {}", path.string());
476 return std::nullopt;
477 }
478
479 std::streamsize file_size = file.tellg();
480 file.seekg(0, std::ios::beg);
481
482 std::vector<unsigned char> file_buffer(file_size);
483 if (!file.read(reinterpret_cast<char*>(file_buffer.data()), file_size)) {
485 "Failed to read image file: {}", path.string());
486 return std::nullopt;
487 }
488 file.close();
489
490 if (extension_of(path) == "exr") {
491 (void)desired_channels;
492 return load_exr_from_memory(file_buffer.data(),
493 static_cast<size_t>(file_size));
494 }
495
496 int width {}, height {}, channels {};
497
498 if (desired_channels == 0) {
499 stbi_info_from_memory(file_buffer.data(), static_cast<int>(file_buffer.size()),
500 &width, &height, &channels);
501 if (channels == 3) {
502 desired_channels = 4;
503 }
504 }
505
506 unsigned char* pixels = stbi_load_from_memory(
507 file_buffer.data(),
508 static_cast<int>(file_buffer.size()),
509 &width, &height, &channels,
510 desired_channels);
511
512 if (!pixels) {
514 "Failed to decode image: {} - {}",
515 path.string(), stbi_failure_reason());
516 return std::nullopt;
517 }
518
519 int result_channels = (desired_channels != 0) ? desired_channels : channels;
520
522 "Loaded image: {} ({}x{}, {} channels{})",
523 path.filename().string(), width, height, result_channels,
524 (channels == 3 && result_channels == 4) ? " [RGB→RGBA]" : "");
525
526 ImageData result;
527 auto& buf = result.pixels.emplace<std::vector<uint8_t>>();
528 size_t data_size = static_cast<size_t>(width) * height * result_channels;
529 buf.resize(data_size);
530 std::memcpy(buf.data(), pixels, data_size);
531
532 result.width = width;
533 result.height = height;
534 result.channels = result_channels;
535
536 switch (result_channels) {
537 case 1:
538 result.format = Portal::Graphics::ImageFormat::R8;
539 break;
540 case 2:
542 break;
543 case 4:
545 break;
546 default:
548 "Unsupported channel count: {}", result_channels);
549 stbi_image_free(pixels);
550 return std::nullopt;
551 }
552
553 stbi_image_free(pixels);
554 return result;
555}
556
557std::optional<ImageData> ImageReader::load(const std::string& path, int desired_channels)
558{
559 return load(std::filesystem::path(path), desired_channels);
560}
561
562std::optional<ImageData> ImageReader::load_from_memory(const void* data, size_t size)
563{
564 if (!data || size == 0) {
566 "Invalid memory buffer for image loading");
567 return std::nullopt;
568 }
569
570 const auto* bytes = static_cast<const unsigned char*>(data);
571
572 if (size >= 4
573 && bytes[0] == 0x76 && bytes[1] == 0x2F
574 && bytes[2] == 0x31 && bytes[3] == 0x01) {
575 return load_exr_from_memory(bytes, size);
576 }
577
578 int width {}, height {}, channels {};
579
580 stbi_info_from_memory(bytes, static_cast<int>(size),
581 &width, &height, &channels);
582
583 int load_as = (channels == 3) ? 4 : 0;
584
585 unsigned char* pixels = stbi_load_from_memory(
586 bytes,
587 static_cast<int>(size),
588 &width, &height, &channels,
589 load_as);
590
591 if (!pixels) {
593 "Failed to decode image from memory: {}",
594 stbi_failure_reason());
595 return std::nullopt;
596 }
597
598 int result_channels = (load_as != 0) ? load_as : channels;
599
601 "Loaded image from memory ({}x{}, {} channels{})",
602 width, height, result_channels,
603 (channels == 3 && result_channels == 4) ? " [RGB→RGBA]" : "");
604
605 ImageData result;
606 auto& buf = result.pixels.emplace<std::vector<uint8_t>>();
607 size_t data_size = static_cast<size_t>(width) * height * result_channels;
608 buf.resize(data_size);
609 std::memcpy(buf.data(), pixels, data_size);
610
611 result.width = width;
612 result.height = height;
613 result.channels = result_channels;
614
615 switch (result_channels) {
616 case 1:
617 result.format = Portal::Graphics::ImageFormat::R8;
618 break;
619 case 2:
621 break;
622 case 4:
624 break;
625 default:
627 "Unsupported channel count: {}", result_channels);
628 stbi_image_free(pixels);
629 return std::nullopt;
630 }
631
632 stbi_image_free(pixels);
633 return result;
634}
635
636std::shared_ptr<Core::VKImage> ImageReader::load_texture(const std::string& path)
637{
638 auto image_data = load(path, 4);
639 if (!image_data) {
640 return nullptr;
641 }
642
644 auto texture = mgr.create_2d(
645 image_data->width,
646 image_data->height,
647 image_data->format,
648 image_data->data());
649
650 if (texture) {
652 "Created GPU texture from image: {}", path);
653 }
654
655 return texture;
656}
657
658std::optional<ImageData> ImageReader::get_image_data() const
659{
660 return m_image_data;
661}
662
663std::shared_ptr<Buffers::TextureBuffer> ImageReader::create_texture_buffer()
664{
665 if (!m_is_open || !m_image_data) {
666 m_last_error = "No image open";
667 return nullptr;
668 }
669
670 auto tex_buffer = std::make_shared<Buffers::TextureBuffer>(
671 m_image_data->width,
672 m_image_data->height,
673 m_image_data->format,
674 m_image_data->data());
675
677 "Created TextureBuffer from image: {}x{} ({} bytes)",
678 m_image_data->width, m_image_data->height, tex_buffer->get_size_bytes());
679
680 return tex_buffer;
681}
682
683bool ImageReader::load_into_buffer(const std::shared_ptr<Buffers::VKBuffer>& buffer)
684{
685 if (!m_is_open || !m_image_data) {
686 m_last_error = "No image open";
687 return false;
688 }
689
690 if (!buffer || !buffer->is_initialized()) {
691 m_last_error = "Invalid or uninitialized buffer";
692 return false;
693 }
694
695 size_t required_size = m_image_data->byte_size();
696 if (buffer->get_size_bytes() < required_size) {
697 m_last_error = "Buffer too small for image data";
698 return false;
699 }
700
702 m_image_data->data(),
703 required_size,
704 buffer);
705
707 "Loaded image into VKBuffer: {}x{} ({} bytes)",
708 m_image_data->width, m_image_data->height, required_size);
709
710 return true;
711}
712
713} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
IO::ImageData image
uint32_t width
const std::vector< float > * pixels
Range size
uint32_t version
static std::string resolve_path(const std::string &filepath)
Resolve a filepath against the project source root if not found as-is.
static std::optional< ImageData > load_from_memory(const void *data, size_t size)
Load image from memory (static utility)
std::type_index get_container_type() const override
Get the container type this reader creates.
std::shared_ptr< Kakshya::SignalSourceContainer > create_container() override
Create and initialize a container from the file.
std::vector< std::string > get_supported_extensions() const override
Get supported file extensions for this reader.
std::type_index get_data_type() const override
Get the data type this reader produces.
static std::optional< ImageData > load(const std::string &path, int desired_channels=4)
Load image from file (static utility)
std::optional< ImageData > m_image_data
bool is_open() const override
Check if a file is currently open.
bool supports_streaming() const override
Check if streaming is supported for the current file.
std::shared_ptr< Buffers::TextureBuffer > create_texture_buffer()
Create a VKBuffer containing the loaded image pixel data.
bool open(const std::string &filepath, FileReadOptions options=FileReadOptions::ALL) override
Open a file for reading.
size_t get_num_dimensions() const override
Get the dimensionality of the file data.
bool load_into_container(std::shared_ptr< Kakshya::SignalSourceContainer > container) override
Load file data into an existing container.
std::vector< Kakshya::DataVariant > read_region(const FileRegion &region) override
Read a specific region of data.
std::vector< Kakshya::DataVariant > read_all() override
Read all data from the file into memory.
std::vector< uint64_t > get_dimension_sizes() const override
Get size of each dimension in the file data.
std::optional< FileMetadata > get_metadata() const override
Get metadata from the open file.
std::vector< FileRegion > get_regions() const override
Get semantic regions from the file.
bool load_into_buffer(const std::shared_ptr< Buffers::VKBuffer > &buffer)
Load image directly into an existing VKBuffer.
bool seek(const std::vector< uint64_t > &position) override
Seek to a specific position in the file.
std::vector< uint64_t > get_read_position() const override
Get current read position in primary dimension.
void close() override
Close the currently open file.
bool can_read(const std::string &filepath) const override
Check if a file can be read by this reader.
std::optional< ImageData > get_image_data() const
Get the loaded image data.
static std::shared_ptr< Core::VKImage > load_texture(const std::string &path)
Load image directly into GPU texture (static utility)
std::string get_last_error() const override
Get the last error message.
uint64_t get_preferred_chunk_size() const override
Get the preferred chunk size for streaming.
void upload_to_gpu(const void *data, size_t size, const std::shared_ptr< VKBuffer > &target, const std::shared_ptr< VKBuffer > &staging)
Upload raw data to GPU buffer (auto-detects host-visible vs device-local)
FileReadOptions
Generic options for file reading behavior.
static bool g_stb_simd_logged
@ FileIO
Filesystem I/O operations.
@ Init
Engine/subsystem initialization.
@ IO
Networking, file handling, streaming.
std::variant< std::vector< double >, std::vector< float >, std::vector< uint8_t >, std::vector< uint16_t >, std::vector< uint32_t >, std::vector< std::complex< float > >, std::vector< std::complex< double > >, std::vector< glm::vec2 >, std::vector< glm::vec3 >, std::vector< glm::vec4 >, std::vector< glm::mat4 > > DataVariant
Multi-type data storage for different precision needs.
Definition NDData.hpp:76
@ IMAGE_COLOR
2D RGB/RGBA image
MAYAFLUX_API TextureLoom & get_texture_manager()
Get the global texture manager instance.
ImageFormat
User-friendly image format enum.
std::unordered_map< std::string, std::any > attributes
Type-specific metadata stored as key-value pairs (e.g., sample rate, channels)
std::string format
File format identifier (e.g., "wav", "mp3", "hdf5")
Generic metadata structure for any file type.
std::vector< uint64_t > start_coordinates
N-dimensional start position (e.g., frame, x, y)
std::vector< uint64_t > end_coordinates
N-dimensional end position (inclusive)
Generic region descriptor for any file type.
bool is_consistent() const
Check that the active variant matches the declared format.
Portal::Graphics::ImageFormat format
Raw image data loaded from file.