Blender V5.0
openimageio_support.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
6#include <OpenImageIO/imagebuf.h>
7#include <OpenImageIO/imagebufalgo.h>
8
9#include <algorithm>
10
11#include "BLI_listbase.h"
12#include "BLI_string_utf8.h"
13
14#include "BKE_idprop.hh"
15
16#include "DNA_ID.h"
17
18#include "IMB_allocimbuf.hh"
20#include "IMB_filetype.hh"
21#include "IMB_metadata.hh"
22
23#include "CLG_log.h"
24
25static CLG_LogRef LOG_READ = {"image.read"};
26static CLG_LogRef LOG_WRITE = {"image.write"};
27
28OIIO_NAMESPACE_USING
29
30using std::string;
31using std::unique_ptr;
32
33namespace blender::imbuf {
34
35/* An OIIO IOProxy used during file packing to write into an in-memory #ImBuf buffer. */
36class ImBufMemWriter : public Filesystem::IOProxy {
37 public:
38 ImBufMemWriter(ImBuf *ibuf) : IOProxy("", Write), ibuf_(ibuf) {}
39
40 const char *proxytype() const override
41 {
42 return "ImBufMemWriter";
43 }
44
45 size_t write(const void *buf, size_t size) override
46 {
47 size = pwrite(buf, size, m_pos);
48 m_pos += size;
49 return size;
50 }
51
52 size_t pwrite(const void *buf, size_t size, int64_t offset) override
53 {
54 /* If buffer is too small increase it. */
55 size_t end = offset + size;
56 while (end > ibuf_->encoded_buffer_size) {
58 /* Out of memory. */
59 return 0;
60 }
61 }
62
63 memcpy(ibuf_->encoded_buffer.data + offset, buf, size);
64
65 ibuf_->encoded_size = std::max<size_t>(end, ibuf_->encoded_size);
66
67 return size;
68 }
69
70 size_t size() const override
71 {
72 return ibuf_->encoded_size;
73 }
74
75 private:
76 ImBuf *ibuf_;
77};
78
79/* Utility to in-place expand an n-component pixel buffer into a 4-component buffer. */
80template<typename T>
81static void fill_all_channels(T *pixels, int width, int height, int components, T alpha)
82{
83 const int64_t pixel_count = int64_t(width) * height;
84 if (components == 3) {
85 for (int64_t i = 0; i < pixel_count; i++) {
86 pixels[i * 4 + 3] = alpha;
87 }
88 }
89 else if (components == 1) {
90 for (int64_t i = 0; i < pixel_count; i++) {
91 pixels[i * 4 + 3] = alpha;
92 pixels[i * 4 + 2] = pixels[i * 4 + 0];
93 pixels[i * 4 + 1] = pixels[i * 4 + 0];
94 }
95 }
96 else if (components == 2) {
97 for (int64_t i = 0; i < pixel_count; i++) {
98 pixels[i * 4 + 3] = pixels[i * 4 + 1];
99 pixels[i * 4 + 2] = pixels[i * 4 + 0];
100 pixels[i * 4 + 1] = pixels[i * 4 + 0];
101 }
102 }
103}
104
105template<typename T>
107 ImageInput *in, int width, int height, int channels, int flags, bool use_all_planes)
108{
109 /* Allocate the ImBuf for the image. */
110 constexpr bool is_float = sizeof(T) > 1;
111 const uint format_flag = (is_float ? IB_float_data : IB_byte_data) | IB_uninitialized_pixels;
112 const uint ibuf_flags = (flags & IB_test) ? 0 : format_flag;
113 const int planes = use_all_planes ? 32 : 8 * channels;
114 ImBuf *ibuf = IMB_allocImBuf(width, height, planes, ibuf_flags);
115 if (!ibuf) {
116 return nullptr;
117 }
118
119 /* No need to load actual pixel data during the test phase. */
120 if (flags & IB_test) {
121 return ibuf;
122 }
123
124 /* Calculate an appropriate stride to read n-channels directly into
125 * the ImBuf 4-channel layout. */
126 const stride_t ibuf_xstride = sizeof(T) * 4;
127 const stride_t ibuf_ystride = ibuf_xstride * width;
128 const TypeDesc format = is_float ? TypeDesc::FLOAT : TypeDesc::UINT8;
129 uchar *rect = is_float ? reinterpret_cast<uchar *>(ibuf->float_buffer.data) :
130 reinterpret_cast<uchar *>(ibuf->byte_buffer.data);
131 void *ibuf_data = rect + ((stride_t(height) - 1) * ibuf_ystride);
132
133 bool ok = in->read_image(
134 0, 0, 0, channels, format, ibuf_data, ibuf_xstride, -ibuf_ystride, AutoStride);
135 if (!ok) {
136 CLOG_ERROR(&LOG_READ, "OpenImageIO read failed: %s", in->geterror().c_str());
137
138 IMB_freeImBuf(ibuf);
139 return nullptr;
140 }
141
142 /* ImBuf always needs 4 channels */
143 const T alpha_fill = is_float ? 1.0f : 0xFF;
144 fill_all_channels<T>(reinterpret_cast<T *>(rect), width, height, channels, alpha_fill);
145
146 return ibuf;
147}
148
149static void set_file_colorspace(ImFileColorSpace &r_colorspace,
150 const ReadContext &ctx,
151 const ImageSpec &spec,
152 bool is_float)
153{
154 /* Guess float data types means HDR colors. File formats can override this later. */
155 r_colorspace.is_hdr_float = is_float;
156
157 /* Override if necessary. */
158 if (ctx.use_metadata_colorspace) {
159 string ics = spec.get_string_attribute("oiio:ColorSpace");
160 STRNCPY_UTF8(r_colorspace.metadata_colorspace, ics.c_str());
161 }
162
163 /* Get colorspace from CICP. */
164 int cicp[4] = {};
165 if (spec.getattribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp, true)) {
168 if (colorspace) {
171 }
172 }
173}
174
178static ImBuf *get_oiio_ibuf(ImageInput *in, const ReadContext &ctx, ImFileColorSpace &r_colorspace)
179{
180 const ImageSpec &spec = in->spec();
181 const int width = spec.width;
182 const int height = spec.height;
183 const bool has_alpha = spec.alpha_channel != -1;
184 const bool is_float = spec.format.basesize() > 1;
185
186 /* Only a maximum of 4 channels are supported by ImBuf. */
187 const int channels = spec.nchannels <= 4 ? spec.nchannels : 4;
188 if (channels < 1) {
189 return nullptr;
190 }
191
192 const bool use_all_planes = has_alpha || ctx.use_all_planes;
193
194 ImBuf *ibuf = nullptr;
195 if (is_float) {
196 ibuf = load_pixels<float>(in, width, height, channels, ctx.flags, use_all_planes);
197 }
198 else {
199 ibuf = load_pixels<uchar>(in, width, height, channels, ctx.flags, use_all_planes);
200 }
201
202 /* Fill in common ibuf properties. */
203 if (ibuf) {
204 ibuf->ftype = ctx.file_type;
205 ibuf->foptions.flag |= (spec.format == TypeDesc::HALF) ? OPENEXR_HALF : 0;
206
207 set_file_colorspace(r_colorspace, ctx, spec, is_float);
208
209 double x_res = spec.get_float_attribute("XResolution", 0.0f);
210 double y_res = spec.get_float_attribute("YResolution", 0.0f);
211 /* Some formats store the resolution as integers. */
212 if (!(x_res > 0.0f && y_res > 0.0f)) {
213 x_res = spec.get_int_attribute("XResolution", 0);
214 y_res = spec.get_int_attribute("YResolution", 0);
215 }
216
217 if (x_res > 0.0f && y_res > 0.0f) {
218 double scale = 1.0;
219 auto unit = spec.get_string_attribute("ResolutionUnit", "");
220 if (ELEM(unit, "in", "inch")) {
221 scale = 100.0 / 2.54;
222 }
223 else if (unit == "cm") {
224 scale = 100.0;
225 }
226 ibuf->ppm[0] = scale * x_res;
227 ibuf->ppm[1] = scale * y_res;
228 }
229
230 /* Transfer metadata to the ibuf if necessary. */
231 if (ctx.flags & IB_metadata) {
233 ibuf->flags |= spec.extra_attribs.empty() ? 0 : IB_metadata;
234
235 for (const auto &attrib : spec.extra_attribs) {
236 if (attrib.name().find("ICCProfile") != string::npos) {
237 continue;
238 }
239 IMB_metadata_set_field(ibuf->metadata, attrib.name().c_str(), attrib.get_string().c_str());
240 }
241 }
242 }
243
244 return ibuf;
245}
246
253 const ImageSpec &config,
254 Filesystem::IOMemReader &mem_reader,
255 ImageSpec &r_newspec)
256{
257 /* Attempt to create a reader based on the passed in format. */
258 unique_ptr<ImageInput> in = ImageInput::create(format);
259 if (!(in && in->valid_file(&mem_reader))) {
260 return nullptr;
261 }
262
263 /* Open the reader using the ioproxy. */
264 in->set_ioproxy(&mem_reader);
265 bool ok = in->open("", r_newspec, config);
266 if (!ok) {
267 return nullptr;
268 }
269
270 return in;
271}
272
273bool imb_oiio_check(const uchar *mem, size_t mem_size, const char *file_format)
274{
275 ImageSpec config, spec;
276
277 /* This memory proxy must remain alive for the full duration of the read. */
278 Filesystem::IOMemReader mem_reader(cspan<uchar>(mem, mem_size));
279 unique_ptr<ImageInput> in = ImageInput::create(file_format);
280 return in && in->valid_file(&mem_reader);
281}
282
284 const ImageSpec &config,
285 ImFileColorSpace &r_colorspace,
286 ImageSpec &r_newspec)
287{
288 /* This memory proxy must remain alive for the full duration of the read. */
289 Filesystem::IOMemReader mem_reader(cspan<uchar>(ctx.mem_start, ctx.mem_size));
290 unique_ptr<ImageInput> in = get_oiio_reader(ctx.file_format, config, mem_reader, r_newspec);
291 if (!in) {
292 return nullptr;
293 }
294
295 return get_oiio_ibuf(in.get(), ctx, r_colorspace);
296}
297
298bool imb_oiio_write(const WriteContext &ctx, const char *filepath, const ImageSpec &file_spec)
299{
300 unique_ptr<ImageOutput> out = ImageOutput::create(ctx.file_format);
301 if (!out) {
302 return false;
303 }
304
305 ImageBuf orig_buf(ctx.mem_spec, ctx.mem_start, ctx.mem_xstride, -ctx.mem_ystride, AutoStride);
306 ImageBuf final_buf{};
307
308#if OIIO_VERSION_MAJOR >= 3
309 const size_t original_channels_count = orig_buf.nchannels();
310#else
311 const int original_channels_count = orig_buf.nchannels();
312#endif
313
314 if (original_channels_count > 1 && file_spec.nchannels == 1) {
315 /* Convert to gray-scale image by computing the luminance. Make sure the weight of alpha
316 * channel is zero since it should not contribute to the luminance. */
317 float weights[4] = {0.0f, 0.0f, 0.0f, 0.0f};
319 ImageBufAlgo::channel_sum(final_buf, orig_buf, {weights, original_channels_count});
320 }
321 else if (original_channels_count == 1 && file_spec.nchannels > 1) {
322 /* Broadcast the gray-scale channel to as many channels as needed, filling the alpha channel
323 * with ones if needed. 0 channel order mean we will be copying from the first channel, while
324 * -1 means we will be filling based on the corresponding value from the defined channel
325 * values. */
326 const int channel_order[] = {0, 0, 0, -1};
327 const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f};
328 const std::string channel_names[] = {"R", "G", "B", "A"};
329 ImageBufAlgo::channels(final_buf,
330 orig_buf,
331 file_spec.nchannels,
332 cspan<int>(channel_order, file_spec.nchannels),
333 cspan<float>(channel_values, file_spec.nchannels),
334 cspan<std::string>(channel_names, file_spec.nchannels));
335 }
336 else if (original_channels_count != file_spec.nchannels) {
337 /* Either trim or fill new channels based on the needed channels count. */
338 int channel_order[4];
339 for (int i = 0; i < 4; i++) {
340 /* If a channel exists in the original buffer, we copy it, if not, we fill it by supplying
341 * -1, which is a special value that means filling based on the value in the defined channels
342 * values. So alpha is filled with 1, and other channels are filled with zero. */
343 const bool channel_exists = i + 1 <= original_channels_count;
344 channel_order[i] = channel_exists ? i : -1;
345 }
346 const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f};
347 const std::string channel_names[] = {"R", "G", "B", "A"};
348 ImageBufAlgo::channels(final_buf,
349 orig_buf,
350 file_spec.nchannels,
351 cspan<int>(channel_order, file_spec.nchannels),
352 cspan<float>(channel_values, file_spec.nchannels),
353 cspan<std::string>(channel_names, file_spec.nchannels));
354 }
355 else {
356 final_buf = std::move(orig_buf);
357 }
358
359 bool write_ok = false;
360 bool close_ok = false;
361 if (ctx.flags & IB_mem) {
362 /* This memory proxy must remain alive until the ImageOutput is finally closed. */
363 ImBufMemWriter writer(ctx.ibuf);
364
366 out->set_ioproxy(&writer);
367 if (out->open("", file_spec)) {
368 write_ok = final_buf.write(out.get());
369 close_ok = out->close();
370 }
371 }
372 else {
373 if (out->open(filepath, file_spec)) {
374 write_ok = final_buf.write(out.get());
375 close_ok = out->close();
376 }
377 }
378
379 const bool all_ok = write_ok && close_ok;
380 if (!all_ok) {
381 CLOG_ERROR(&LOG_WRITE, "OpenImageIO write failed: %s", out->geterror().c_str());
382 errno = 0; /* Prevent higher level layers from calling `perror` unnecessarily. */
383 }
384
385 return all_ok;
386}
387
389 ImBuf *ibuf,
390 int flags,
391 bool prefer_float)
392{
393 WriteContext ctx{};
394 ctx.file_format = file_format;
395 ctx.ibuf = ibuf;
396 ctx.flags = flags;
397
398 const int width = ibuf->x;
399 const int height = ibuf->y;
400 const bool use_float = prefer_float && (ibuf->float_buffer.data != nullptr);
401 if (use_float) {
402 const int mem_channels = ibuf->channels ? ibuf->channels : 4;
403 ctx.mem_xstride = sizeof(float) * mem_channels;
404 ctx.mem_ystride = width * ctx.mem_xstride;
405 ctx.mem_start = reinterpret_cast<uchar *>(ibuf->float_buffer.data);
406 ctx.mem_spec = ImageSpec(width, height, mem_channels, TypeDesc::FLOAT);
407 }
408 else {
409 const int mem_channels = 4;
410 ctx.mem_xstride = sizeof(uchar) * mem_channels;
411 ctx.mem_ystride = width * ctx.mem_xstride;
412 ctx.mem_start = ibuf->byte_buffer.data;
413 ctx.mem_spec = ImageSpec(width, height, mem_channels, TypeDesc::UINT8);
414 }
415
416 /* We always write using a negative y-stride so ensure we start at the end. */
417 ctx.mem_start = ctx.mem_start + ((stride_t(height) - 1) * ctx.mem_ystride);
418
419 return ctx;
420}
421
422ImageSpec imb_create_write_spec(const WriteContext &ctx, int file_channels, TypeDesc data_format)
423{
424 const int width = ctx.ibuf->x;
425 const int height = ctx.ibuf->y;
426 ImageSpec file_spec(width, height, file_channels, data_format);
427
428 /* Populate the spec with all common attributes.
429 *
430 * Care must be taken with the metadata:
431 * - It should be processed first, before the "Resolution" metadata below, to
432 * ensure the proper values end up in the #ImageSpec
433 * - It needs to filter format-specific metadata that may no longer apply to
434 * the current format being written (e.g. metadata for tiff being written to a `PNG`)
435 */
436
437 if (ctx.ibuf->metadata) {
439 if (prop->type == IDP_STRING) {
440 /* If this property has a prefixed name (oiio:, tiff:, etc.) and it belongs to
441 * oiio or a different format, then skip. */
442 if (char *colon = strchr(prop->name, ':')) {
443 std::string prefix(prop->name, colon);
444 Strutil::to_lower(prefix);
445 if (prefix == "oiio" ||
446 (!STREQ(prefix.c_str(), ctx.file_format) && OIIO::is_imageio_format_name(prefix)))
447 {
448 /* Skip this attribute. */
449 continue;
450 }
451 }
452
453 file_spec.attribute(prop->name, IDP_string_get(prop));
454 }
455 }
456 }
457
458 if (ctx.ibuf->ppm[0] > 0.0 && ctx.ibuf->ppm[1] > 0.0) {
459 if (STREQ(ctx.file_format, "bmp")) {
460 /* BMP only supports meters as integers. */
461 file_spec.attribute("ResolutionUnit", "m");
462 file_spec.attribute("XResolution", int(round(ctx.ibuf->ppm[0])));
463 file_spec.attribute("YResolution", int(round(ctx.ibuf->ppm[1])));
464 }
465 else {
466 /* More OIIO formats support inch than meter. */
467 file_spec.attribute("ResolutionUnit", "in");
468 file_spec.attribute("XResolution", float(ctx.ibuf->ppm[0] * 0.0254));
469 file_spec.attribute("YResolution", float(ctx.ibuf->ppm[1] * 0.0254));
470 }
471 }
472
473 /* Write ICC profile and/or CICP if there is one associated with the colorspace. */
474 const ColorSpace *colorspace = (ctx.mem_spec.format == TypeDesc::FLOAT) ?
477 if (colorspace) {
479 if (!icc_profile.is_empty()) {
480 file_spec.attribute("ICCProfile",
481 OIIO::TypeDesc(OIIO::TypeDesc::UINT8, icc_profile.size()),
482 icc_profile.data());
483 }
484
485 /* PNG only supports RGB matrix. For AVIF and HEIF we want to use a YUV matrix
486 * as these are based on video codecs designed to use them. */
487 const bool rgb_matrix = STREQ(ctx.file_format, "png");
488 int cicp[4];
490 colorspace, ColorManagedFileOutput::Image, rgb_matrix, cicp))
491 {
492 file_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp);
493 }
494 }
495
496 return file_spec;
497}
498
499} // namespace blender::imbuf
#define IDP_string_get(prop)
blender::ocio::ColorSpace ColorSpace
Definition BLF_api.hh:38
#define LISTBASE_FOREACH(type, var, list)
#define STRNCPY_UTF8(dst, src)
unsigned char uchar
unsigned int uint
#define ELEM(...)
#define STREQ(a, b)
#define CLOG_ERROR(clg_ref,...)
Definition CLG_log.h:188
ID and Library types, which are fundamental for SDNA.
@ IDP_STRING
bool IMB_colormanagement_space_to_cicp(const ColorSpace *colorspace, const ColorManagedFileOutput output, const bool rgb_matrix, int cicp[4])
const char * IMB_colormanagement_colorspace_get_name(const ColorSpace *colorspace)
blender::Vector< char > IMB_colormanagement_space_to_icc_profile(const ColorSpace *colorspace)
const ColorSpace * IMB_colormanagement_space_from_cicp(const int cicp[4], const ColorManagedFileOutput output)
BLI_INLINE void IMB_colormanagement_get_luminance_coefficients(float r_rgb[3])
void IMB_freeImBuf(ImBuf *ibuf)
ImBuf * IMB_allocImBuf(unsigned int x, unsigned int y, unsigned char planes, unsigned int flags)
@ IB_float_data
@ IB_byte_data
@ IB_uninitialized_pixels
@ IB_metadata
@ IB_mem
@ IB_test
void IMB_metadata_set_field(IDProperty *metadata, const char *key, const char *value)
Definition metadata.cc:68
void IMB_metadata_ensure(IDProperty **metadata)
Definition metadata.cc:23
bool imb_enlargeencodedbufferImBuf(ImBuf *ibuf)
bool imb_addencodedbufferImBuf(ImBuf *ibuf)
long long int int64_t
int64_t size() const
bool is_empty() const
size_t write(const void *buf, size_t size) override
size_t pwrite(const void *buf, size_t size, int64_t offset) override
const char * proxytype() const override
nullptr float
#define in
#define out
#define round
#define OPENEXR_HALF
format
#define T
bool imb_oiio_write(const WriteContext &ctx, const char *filepath, const ImageSpec &file_spec)
WriteContext imb_create_write_context(const char *file_format, ImBuf *ibuf, int flags, bool prefer_float)
static unique_ptr< ImageInput > get_oiio_reader(const char *format, const ImageSpec &config, Filesystem::IOMemReader &mem_reader, ImageSpec &r_newspec)
ImBuf * imb_oiio_read(const ReadContext &ctx, const ImageSpec &config, ImFileColorSpace &r_colorspace, ImageSpec &r_newspec)
bool imb_oiio_check(const uchar *mem, size_t mem_size, const char *file_format)
ImageSpec imb_create_write_spec(const WriteContext &ctx, int file_channels, TypeDesc data_format)
static ImBuf * get_oiio_ibuf(ImageInput *in, const ReadContext &ctx, ImFileColorSpace &r_colorspace)
static ImBuf * load_pixels(ImageInput *in, int width, int height, int channels, int flags, bool use_all_planes)
static void fill_all_channels(T *pixels, int width, int height, int components, T alpha)
static void set_file_colorspace(ImFileColorSpace &r_colorspace, const ReadContext &ctx, const ImageSpec &spec, bool is_float)
static CLG_LogRef LOG_WRITE
static CLG_LogRef LOG_READ
ListBase group
Definition DNA_ID.h:143
char name[64]
Definition DNA_ID.h:164
IDPropertyData data
Definition DNA_ID.h:169
const ColorSpace * colorspace
const ColorSpace * colorspace
ImBufFloatBuffer float_buffer
ImbFormatOptions foptions
ImBufByteBuffer byte_buffer
enum eImbFileType ftype
IDProperty * metadata
double ppm[2]
char metadata_colorspace[IM_MAX_SPACE]
i
Definition text_draw.cc:230