MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
EXRWriter.cpp
Go to the documentation of this file.
1#include "EXRWriter.hpp"
2
3#include "FileWriter.hpp"
4
6
7#include <tinyexr.h>
8
9#include <fstream>
10
11namespace MayaFlux::IO {
12
13namespace {
14
15 std::string extension_of(const std::string& filepath)
16 {
17 auto ext = std::filesystem::path(filepath).extension().string();
18 if (!ext.empty() && ext[0] == '.') {
19 ext = ext.substr(1);
20 }
21 std::ranges::transform(ext, ext.begin(),
22 [](unsigned char c) { return std::tolower(c); });
23 return ext;
24 }
25
26 bool flush_to_disk(const std::string& filepath, const unsigned char* bytes, size_t size)
27 {
28 const auto resolved = resolve_write_path(filepath);
29 std::ofstream out(resolved, std::ios::binary | std::ios::trunc);
30 if (!out.is_open()) {
31 return false;
32 }
33 out.write(reinterpret_cast<const char*>(bytes),
34 static_cast<std::streamsize>(size));
35 return out.good();
36 }
37
38 int resolve_compression(int requested)
39 {
40 if (requested < 0) {
41 return TINYEXR_COMPRESSIONTYPE_ZIP;
42 }
43 switch (requested) {
44 case TINYEXR_COMPRESSIONTYPE_NONE:
45 case TINYEXR_COMPRESSIONTYPE_RLE:
46 case TINYEXR_COMPRESSIONTYPE_ZIPS:
47 case TINYEXR_COMPRESSIONTYPE_ZIP:
48 case TINYEXR_COMPRESSIONTYPE_PIZ:
49 return requested;
50 default:
51 return TINYEXR_COMPRESSIONTYPE_ZIP;
52 }
53 }
54
55} // namespace
56
57// ============================================================================
58// Registry hook
59// ============================================================================
60
62{
64 reg.register_writer(
65 { "exr" },
66 []() -> std::unique_ptr<ImageWriter> {
67 return std::make_unique<EXRWriter>();
68 });
69
71 "EXRWriter registered for: exr");
72}
73
74// ============================================================================
75// Public interface
76// ============================================================================
77
78bool EXRWriter::can_write(const std::string& filepath) const
79{
80 return extension_of(filepath) == "exr";
81}
82
83std::vector<std::string> EXRWriter::get_supported_extensions() const
84{
85 return { "exr" };
86}
87
88// ============================================================================
89// Channel name resolution
90// ============================================================================
91
92std::vector<std::string> EXRWriter::resolve_channel_names(
93 const ImageData& data, const ImageWriteOptions& options) const
94{
95 if (!options.channel_names.empty()) {
96 if (options.channel_names.size() != data.channels) {
98 "EXRWriter: channel_names size ({}) does not match channels ({}); "
99 "falling back to defaults",
100 options.channel_names.size(), data.channels);
101 } else {
102 return options.channel_names;
103 }
104 }
105
106 switch (data.channels) {
107 case 1:
108 return { "Y" };
109 case 2:
110 return { "Y", "A" };
111 case 3:
112 return { "B", "G", "R" };
113 case 4:
114 return { "A", "B", "G", "R" };
115 default: {
116 std::vector<std::string> names;
117 names.reserve(data.channels);
118 for (uint32_t i = 0; i < data.channels; ++i) {
119 names.emplace_back("C" + std::to_string(i));
120 }
121 return names;
122 }
123 }
124}
125
126// ============================================================================
127// Deinterleave
128// ============================================================================
129
130std::vector<std::vector<float>> EXRWriter::deinterleave(
131 const std::vector<float>& src,
132 uint32_t width, uint32_t height, uint32_t channels,
133 bool flip_vertically)
134{
135 const size_t pixel_count = static_cast<size_t>(width) * height;
136 std::vector<std::vector<float>> planes(channels, std::vector<float>(pixel_count));
137
138 for (uint32_t y = 0; y < height; ++y) {
139 const uint32_t src_row = flip_vertically ? (height - 1 - y) : y;
140 const size_t src_row_offset = static_cast<size_t>(src_row) * width * channels;
141 const size_t dst_row_offset = static_cast<size_t>(y) * width;
142
143 for (uint32_t x = 0; x < width; ++x) {
144 const size_t src_pixel = src_row_offset + static_cast<size_t>(x) * channels;
145 const size_t dst_pixel = dst_row_offset + x;
146
147 for (uint32_t c = 0; c < channels; ++c) {
148 planes[c][dst_pixel] = src[src_pixel + c];
149 }
150 }
151 }
152
153 return planes;
154}
155
156// ============================================================================
157// Write
158// ============================================================================
159
161 const std::string& filepath,
162 const ImageData& data,
163 const ImageWriteOptions& options)
164{
165 m_last_error.clear();
166
167 if (!data.is_consistent()) {
168 m_last_error = "ImageData variant does not match declared ImageFormat";
170 return false;
171 }
172
173 if (data.width == 0 || data.height == 0 || data.channels == 0) {
174 m_last_error = "ImageData has zero dimensions";
176 return false;
177 }
178
179 const auto* src = data.as_float();
180 if (!src) {
181 m_last_error = "EXR requires float ImageData (uint8 and uint16 not supported)";
183 return false;
184 }
185
186 const size_t expected_elements = static_cast<size_t>(data.width) * data.height * data.channels;
187 if (src->size() < expected_elements) {
188 m_last_error = "EXR source buffer too small: expected "
189 + std::to_string(expected_elements)
190 + " got " + std::to_string(src->size());
192 return false;
193 }
194
195 const auto names = resolve_channel_names(data, options);
196 auto planes = deinterleave(*src, data.width, data.height, data.channels,
197 options.flip_vertically);
198
199 // ------------------------------------------------------------------------
200 // Populate EXRImage
201 // ------------------------------------------------------------------------
202 EXRImage image;
203 InitEXRImage(&image);
204
205 image.num_channels = static_cast<int>(data.channels);
206 image.width = static_cast<int>(data.width);
207 image.height = static_cast<int>(data.height);
208
209 std::vector<float*> plane_ptrs(data.channels);
210 for (uint32_t c = 0; c < data.channels; ++c) {
211 plane_ptrs[c] = planes[c].data();
212 }
213 image.images = reinterpret_cast<unsigned char**>(plane_ptrs.data());
214
215 // ------------------------------------------------------------------------
216 // Populate EXRHeader
217 // ------------------------------------------------------------------------
218 EXRHeader header;
219 InitEXRHeader(&header);
220
221 header.num_channels = static_cast<int>(data.channels);
222 header.compression_type = resolve_compression(options.compression);
223
224 std::vector<EXRChannelInfo> channel_infos(data.channels);
225 std::vector<int> pixel_types(data.channels, TINYEXR_PIXELTYPE_FLOAT);
226 std::vector<int> requested_pixel_types(data.channels, TINYEXR_PIXELTYPE_FLOAT);
227
228 for (uint32_t c = 0; c < data.channels; ++c) {
229 std::memset(&channel_infos[c], 0, sizeof(EXRChannelInfo));
230 const std::string& n = names[c];
231 const size_t copy_len = std::min<size_t>(n.size(), 255);
232 std::memcpy(channel_infos[c].name, n.data(), copy_len);
233 channel_infos[c].name[copy_len] = '\0';
234 }
235
236 header.channels = channel_infos.data();
237 header.pixel_types = pixel_types.data();
238 header.requested_pixel_types = requested_pixel_types.data();
239
240 // ------------------------------------------------------------------------
241 // Encode to memory, then flush
242 // ------------------------------------------------------------------------
243 unsigned char* encoded = nullptr;
244 const char* err = nullptr;
245
246 const size_t encoded_size = SaveEXRImageToMemory(
247 &image, &header, &encoded, &err);
248
249 if (encoded_size == 0 || encoded == nullptr) {
250 m_last_error = err ? std::string("tinyexr: ") + err
251 : "SaveEXRImageToMemory returned 0 bytes";
252 if (err) {
253 FreeEXRErrorMessage(err);
254 }
256 return false;
257 }
258
259 const bool ok = flush_to_disk(filepath, encoded, encoded_size);
260
261 std::free(encoded);
262
263 if (!ok) {
264 m_last_error = "Failed to write EXR file: " + filepath;
266 return false;
267 }
268
270 "Wrote EXR: {} ({}x{}, {} channels, compression {}, {} bytes)",
271 filepath, data.width, data.height, data.channels,
272 header.compression_type, encoded_size);
273
274 return true;
275}
276
277} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
IO::ImageData image
Definition Decoder.cpp:57
uint32_t width
Definition Decoder.cpp:59
std::vector< std::string > resolve_channel_names(const ImageData &data, const ImageWriteOptions &options) const
Resolve channel name list based on options and channel count.
Definition EXRWriter.cpp:92
static std::vector< std::vector< float > > deinterleave(const std::vector< float > &src, uint32_t width, uint32_t height, uint32_t channels, bool flip_vertically)
Deinterleave source float buffer into N planar channel buffers.
bool write(const std::string &filepath, const ImageData &data, const ImageWriteOptions &options={}) override
Write image data to disk.
std::vector< std::string > get_supported_extensions() const override
File extensions handled by this writer (without dot).
Definition EXRWriter.cpp:83
bool can_write(const std::string &filepath) const override
Check whether this writer handles the given filepath.
Definition EXRWriter.cpp:78
static void register_with_registry()
Register this writer with the ImageWriterRegistry.
Definition EXRWriter.cpp:61
static ImageWriterRegistry & instance()
std::string resolve_write_path(const std::string &filepath)
Anchor a relative output path to Config::SOURCE_DIR.
@ FileIO
Filesystem I/O operations.
@ Init
Engine/subsystem initialization.
@ IO
Networking, file handling, streaming.
bool is_consistent() const
Check that the active variant matches the declared format.
const std::vector< float > * as_float() const
Raw image data loaded from file.
std::vector< std::string > channel_names
Configuration for image writing.