10#ifdef MAYAFLUX_ARCH_X64
12#ifdef MAYAFLUX_COMPILER_MSVC
17#ifdef MAYAFLUX_ARCH_ARM64
21#define STBI_NO_FAILURE_STRINGS
22#define STB_IMAGE_IMPLEMENTATION
25#if __has_include("stb/stb_image.h")
26#include "stb/stb_image.h"
27#elif __has_include("stb_image.h")
30#error "stb_image.h not found"
42 std::string extension_of(
const std::filesystem::path& path)
44 auto ext = path.extension().string();
45 if (!ext.empty() && ext[0] ==
'.') {
48 std::ranges::transform(ext, ext.begin(),
49 [](
unsigned char c) { return std::tolower(c); });
53 std::optional<ImageData> crop_image(
55 const Kakshya::Region& region)
57 const auto& start = region.start_coordinates;
58 const auto& end = region.end_coordinates;
59 if (start.size() != 2 || end.size() != 2
60 || start[0] > end[0] || start[1] > end[1]
61 || end[0] >=
image.width || end[1] >=
image.height) {
63 "Image crop region is outside the decoded image");
67 const auto width =
static_cast<uint32_t
>(end[0] - start[0] + 1);
68 const auto height =
static_cast<uint32_t
>(end[1] - start[1] + 1);
74 std::visit([&](
const auto& source) {
75 using Storage = std::decay_t<
decltype(source)>;
76 using Element =
typename Storage::value_type;
77 auto& target = std::get<Storage>(cropped->pixels);
78 const size_t row_elements =
static_cast<size_t>(
width) *
image.channels;
79 for (uint32_t y = 0; y <
height; ++y) {
80 const size_t source_offset =
81 (
static_cast<size_t>(start[1] + y) *
image.width + start[0]) *
image.channels;
82 std::memcpy(target.data() +
static_cast<size_t>(y) * row_elements,
83 source.data() + source_offset, row_elements *
sizeof(Element));
104 std::optional<ImageData> load_exr_from_memory(
105 const unsigned char* bytes,
size_t size)
110 if (ParseEXRVersionFromMemory(&version, bytes,
size) != TINYEXR_SUCCESS) {
112 "EXR: failed to parse version");
115 if (version.multipart || version.non_image) {
117 "EXR: multipart and deep images are not supported");
122 InitEXRHeader(&header);
124 const char* err =
nullptr;
125 if (ParseEXRHeaderFromMemory(&header, &version, bytes,
size, &err)
126 != TINYEXR_SUCCESS) {
128 "EXR: header parse failed: {}", err ? err :
"unknown");
130 FreeEXRErrorMessage(err);
134 for (
int i = 0; i < header.num_channels; ++i) {
135 if (header.pixel_types[i] == TINYEXR_PIXELTYPE_UINT) {
137 "EXR: uint pixel type channel '{}' not supported",
138 header.channels[i].name);
139 FreeEXRHeader(&header);
142 header.requested_pixel_types[i] = TINYEXR_PIXELTYPE_FLOAT;
146 InitEXRImage(&
image);
148 if (LoadEXRImageFromMemory(&
image, &header, bytes,
size, &err)
149 != TINYEXR_SUCCESS) {
151 "EXR: image load failed: {}", err ? err :
"unknown");
153 FreeEXRErrorMessage(err);
154 FreeEXRHeader(&header);
158 const auto width =
static_cast<uint32_t
>(
image.width);
159 const auto height =
static_cast<uint32_t
>(
image.height);
160 const auto src_channels =
static_cast<uint32_t
>(
image.num_channels);
162 if (
width == 0 ||
height == 0 || src_channels == 0) {
164 "EXR: zero dimensions or channels");
165 FreeEXRImage(&
image);
166 FreeEXRHeader(&header);
170 uint32_t out_channels = src_channels;
172 switch (src_channels) {
188 "EXR: {} channels not supported (1/2/3/4 only)", src_channels);
189 FreeEXRImage(&
image);
190 FreeEXRHeader(&header);
194 const size_t pixel_count =
static_cast<size_t>(
width) *
height;
195 const size_t element_count = pixel_count * out_channels;
198 auto&
dst = result.
pixels.emplace<std::vector<float>>(element_count, 0.0F);
200 auto** planes =
reinterpret_cast<float**
>(
image.images);
202 for (
size_t p = 0; p < pixel_count; ++p) {
203 for (uint32_t c = 0; c < src_channels; ++c) {
204 dst[p * out_channels + c] = planes[c][p];
206 if (out_channels > src_channels) {
207 dst[p * out_channels + (out_channels - 1)] = 1.0F;
211 result.width =
width;
213 result.channels = out_channels;
217 "Loaded EXR: {}x{}, {} channels{}",
219 (src_channels == 3) ?
" [BGR→ABGR, A=1.0]" :
"");
221 FreeEXRImage(&
image);
222 FreeEXRHeader(&header);
232 "STB Image: SSE2 SIMD optimizations enabled (x64)");
233#elif defined(STBI_NEON)
235 "STB Image: NEON SIMD optimizations enabled (ARM64)");
238 "STB Image: No SIMD optimizations (scalar fallback)");
244 std::optional<Kakshya::Region> region)
245 : m_desired_channels(desired_channels)
246 , m_region(
std::move(region))
258 auto ext = std::filesystem::path(filepath).extension().string();
259 if (!ext.empty() && ext[0] ==
'.') {
263 static const std::vector<std::string> supported = {
264 "png",
"jpg",
"jpeg",
"bmp",
"tga",
"psd",
"gif",
"hdr",
"pic",
"pnm",
268 return std::ranges::find(supported, ext) != supported.end();
296 "Opened image: {} ({}x{}, {} channels)",
346 [](
const auto& vec) -> std::vector<Kakshya::DataVariant> {
374 uint32_t region_width = x_end - x_start;
375 uint32_t region_height = y_end - y_start;
379 const size_t pixel_stride_bytes = bytes_per_elem *
m_image_data->channels;
381 std::vector<uint8_t> region_data(
382 static_cast<size_t>(region_width) * region_height * pixel_stride_bytes);
386 for (uint32_t y = 0; y < region_height; ++y) {
387 size_t src_offset = (
static_cast<size_t>((y_start + y) *
m_image_data->width + x_start)) * pixel_stride_bytes;
388 size_t dst_offset =
static_cast<size_t>(y * region_width) * pixel_stride_bytes;
389 size_t row_size =
static_cast<size_t>(region_width) * pixel_stride_bytes;
391 region_data.data() + dst_offset,
396 return { region_data };
401 m_last_error =
"Images use direct GPU texture creation, not containers";
407 m_last_error =
"Images cannot be loaded into SignalSourceContainer";
423 return {
"png",
"jpg",
"jpeg",
"bmp",
"tga",
"psd",
"gif",
"hdr",
"pic",
"pnm",
429 return typeid(std::vector<uint8_t>);
470 const std::filesystem::path& path,
int desired_channels,
471 std::optional<Kakshya::Region> region)
473 auto resolved = std::filesystem::path(
resolve_path(path.string()));
474 if (!std::filesystem::exists(resolved)) {
476 "Image file not found: {}", resolved.string());
480 std::ifstream file(resolved, std::ios::binary | std::ios::ate);
481 if (!file.is_open()) {
483 "Failed to open image file: {}", resolved.string());
487 std::streamsize file_size = file.tellg();
488 file.seekg(0, std::ios::beg);
490 std::vector<unsigned char> file_buffer(file_size);
491 if (!file.read(
reinterpret_cast<char*
>(file_buffer.data()), file_size)) {
493 "Failed to read image file: {}", resolved.string());
498 if (extension_of(resolved) ==
"exr") {
499 (void)desired_channels;
500 auto image = load_exr_from_memory(file_buffer.data(),
static_cast<size_t>(file_size));
501 if (
image && region) {
502 return crop_image(*
image, *region);
509 if (desired_channels == 0) {
510 stbi_info_from_memory(file_buffer.data(),
static_cast<int>(file_buffer.size()),
513 desired_channels = 4;
517 unsigned char*
pixels = stbi_load_from_memory(
519 static_cast<int>(file_buffer.size()),
525 "Failed to decode image: {} - {}",
526 path.string(), stbi_failure_reason());
530 int result_channels = (desired_channels != 0) ? desired_channels :
channels;
533 "Loaded image: {} ({}x{}, {} channels{})",
534 resolved.filename().string(),
width,
height, result_channels,
535 (
channels == 3 && result_channels == 4) ?
" [RGB→RGBA]" :
"");
538 auto& buf = result.
pixels.emplace<std::vector<uint8_t>>();
539 size_t data_size =
static_cast<size_t>(
width) *
height * result_channels;
540 buf.resize(data_size);
541 std::memcpy(buf.data(),
pixels, data_size);
543 result.width =
width;
545 result.channels = result_channels;
547 switch (result_channels) {
549 result.format = Portal::Graphics::ImageFormat::R8;
552 result.format = Portal::Graphics::ImageFormat::RG8;
555 result.format = Portal::Graphics::ImageFormat::RGBA8;
559 "Unsupported channel count: {}", result_channels);
566 return crop_image(result, *region);
572 const std::string& path,
int desired_channels,
573 std::optional<Kakshya::Region> region)
575 return load(std::filesystem::path(path), desired_channels, std::move(region));
580 if (!data ||
size == 0) {
582 "Invalid memory buffer for image loading");
586 const auto* bytes =
static_cast<const unsigned char*
>(data);
589 && bytes[0] == 0x76 && bytes[1] == 0x2F
590 && bytes[2] == 0x31 && bytes[3] == 0x01) {
591 return load_exr_from_memory(bytes,
size);
596 stbi_info_from_memory(bytes,
static_cast<int>(
size),
599 int load_as = (
channels == 3) ? 4 : 0;
601 unsigned char*
pixels = stbi_load_from_memory(
603 static_cast<int>(
size),
609 "Failed to decode image from memory: {}",
610 stbi_failure_reason());
614 int result_channels = (load_as != 0) ? load_as :
channels;
617 "Loaded image from memory ({}x{}, {} channels{})",
619 (
channels == 3 && result_channels == 4) ?
" [RGB→RGBA]" :
"");
622 auto& buf = result.
pixels.emplace<std::vector<uint8_t>>();
623 size_t data_size =
static_cast<size_t>(
width) *
height * result_channels;
624 buf.resize(data_size);
625 std::memcpy(buf.data(),
pixels, data_size);
627 result.width =
width;
629 result.channels = result_channels;
631 switch (result_channels) {
633 result.format = Portal::Graphics::ImageFormat::R8;
636 result.format = Portal::Graphics::ImageFormat::RG8;
639 result.format = Portal::Graphics::ImageFormat::RGBA8;
643 "Unsupported channel count: {}", result_channels);
653 const std::string& path,
int desired_channels,
654 std::optional<Kakshya::Region> region)
656 auto image_data =
load(path, desired_channels, std::move(region));
662 auto texture = mgr.create_2d(
670 "Created GPU texture from image: {}", path);
688 auto tex_buffer = std::make_shared<Buffers::TextureBuffer>(
693 "Created TextureBuffer from image: {}x{} ({} bytes)",
712 if (
buffer->get_size_bytes() < required_size) {
723 "Loaded image into VKBuffer: {}x{} ({} bytes)",
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
const std::vector< float > * pixels
std::shared_ptr< NetworkGeometryBuffer > buffer
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, std::optional< Kakshya::Region > region=std::nullopt)
Load image from file (static utility)
static std::shared_ptr< Core::VKImage > load_texture(const std::string &path, int desired_channels=4, std::optional< Kakshya::Region > region=std::nullopt)
Load image directly into GPU texture (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 ®ion) override
Read a specific region of data.
std::optional< Kakshya::Region > m_region
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.
ImageReader(int desired_channels=4, std::optional< Kakshya::Region > region=std::nullopt)
Configure channel conversion and an optional source-pixel crop for open().
std::optional< ImageData > get_image_data() const
Get the loaded image data.
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, size_t dst_offset)
Upload raw data to GPU buffer (auto-detects host-visible vs device-local)
FileReadOptions
Generic options for file reading behavior.
Kakshya::ImageData ImageData
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.
@ IMAGE_COLOR
2D RGB/RGBA image
MAYAFLUX_API TextureLoom & get_texture_manager()
Get the global texture manager instance.
Kakshya::ImageFormat ImageFormat
Portal name for Kakshya::ImageFormat, which maps to vk::Format in TextureLoom.
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.
static std::optional< ImageData > allocate(uint32_t width, uint32_t height, uint32_t channels, ImageFormat format)
Allocate zeroed pixel storage of the variant format calls for.
Raw image data loaded from file or read back from a GPU image.