Previously it was only working for the single layer case. For multipart we write the colorspace in each part. For single part we write the first non-data colorspace, and hope data passes will be identified based on channel name like Blender does (e.g. XYZ instead of RGB). Reading is unchanged and still the same as before, in that it only reads the colorspace from the first part. There is only one color space per image datablock, so we can not store anything more currently. In practice it would be unusual for all passes in a file not to either have the same colorspace or be data. All the compositor file output test images were updated to include the metadata, so that the test will check if the metadata is there. Ref #144911 Pull Request: https://projects.blender.org/blender/blender/pulls/146809
2384 lines
72 KiB
C++
2384 lines
72 KiB
C++
/* SPDX-FileCopyrightText: 2005 `Gernot Ziegler <gz@lysator.liu.se>`. All rights reserved.
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup openexr
|
|
*/
|
|
|
|
#include "IMB_filetype.hh"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cerrno>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <fcntl.h>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <set>
|
|
#include <string>
|
|
|
|
/* The OpenEXR version can reliably be found in this header file from OpenEXR,
|
|
* for both 2.x and 3.x:
|
|
*/
|
|
#include <OpenEXR/OpenEXRConfig.h>
|
|
#define COMBINED_OPENEXR_VERSION \
|
|
((10000 * OPENEXR_VERSION_MAJOR) + (100 * OPENEXR_VERSION_MINOR) + OPENEXR_VERSION_PATCH)
|
|
|
|
#if COMBINED_OPENEXR_VERSION >= 20599
|
|
/* >=2.5.99 -> OpenEXR >=3.0 */
|
|
# include <Imath/half.h>
|
|
# include <OpenEXR/ImfFrameBuffer.h>
|
|
# define exr_file_offset_t uint64_t
|
|
#else
|
|
/* OpenEXR 2.x, use the old locations. */
|
|
# include <OpenEXR/half.h>
|
|
# define exr_file_offset_t Int64
|
|
#endif
|
|
|
|
#include <OpenEXR/Iex.h>
|
|
#include <OpenEXR/ImfArray.h>
|
|
#include <OpenEXR/ImfAttribute.h>
|
|
#include <OpenEXR/ImfChannelList.h>
|
|
#include <OpenEXR/ImfChromaticities.h>
|
|
#include <OpenEXR/ImfCompression.h>
|
|
#include <OpenEXR/ImfCompressionAttribute.h>
|
|
#include <OpenEXR/ImfIO.h>
|
|
#include <OpenEXR/ImfInputFile.h>
|
|
#include <OpenEXR/ImfIntAttribute.h>
|
|
#include <OpenEXR/ImfOutputFile.h>
|
|
#include <OpenEXR/ImfPixelType.h>
|
|
#include <OpenEXR/ImfPreviewImage.h>
|
|
#include <OpenEXR/ImfRgbaFile.h>
|
|
#include <OpenEXR/ImfStandardAttributes.h>
|
|
#include <OpenEXR/ImfStringAttribute.h>
|
|
#include <OpenEXR/ImfVersion.h>
|
|
|
|
/* multiview/multipart */
|
|
#include <OpenEXR/ImfInputPart.h>
|
|
#include <OpenEXR/ImfMultiPartInputFile.h>
|
|
#include <OpenEXR/ImfMultiPartOutputFile.h>
|
|
#include <OpenEXR/ImfMultiView.h>
|
|
#include <OpenEXR/ImfOutputPart.h>
|
|
#include <OpenEXR/ImfPartHelper.h>
|
|
#include <OpenEXR/ImfPartType.h>
|
|
#include <OpenEXR/ImfTiledOutputPart.h>
|
|
|
|
#include "DNA_scene_types.h" /* For OpenEXR compression constants */
|
|
|
|
#include <openexr_api.h>
|
|
|
|
#if defined(WIN32)
|
|
# include "utfconv.hh"
|
|
# include <io.h>
|
|
#else
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "BLI_fileops.h"
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_math_base.hh"
|
|
#include "BLI_math_color.h"
|
|
#include "BLI_mmap.h"
|
|
#include "BLI_string.h"
|
|
#include "BLI_string_ref.hh"
|
|
#include "BLI_string_utf8.h"
|
|
#include "BLI_threads.h"
|
|
|
|
#include "BKE_blender_version.h"
|
|
#include "BKE_idprop.hh"
|
|
#include "BKE_image.hh"
|
|
|
|
#include "CLG_log.h"
|
|
|
|
#include "IMB_allocimbuf.hh"
|
|
#include "IMB_colormanagement.hh"
|
|
#include "IMB_imbuf.hh"
|
|
#include "IMB_imbuf_types.hh"
|
|
#include "IMB_metadata.hh"
|
|
#include "IMB_openexr.hh"
|
|
|
|
static CLG_LogRef LOG = {"image.openexr"};
|
|
|
|
using namespace Imf;
|
|
using namespace Imath;
|
|
|
|
/* prototype */
|
|
static bool exr_has_multiview(MultiPartInputFile &file);
|
|
static bool exr_has_multipart_file(MultiPartInputFile &file);
|
|
static bool exr_has_alpha(MultiPartInputFile &file);
|
|
static const ColorSpace *imb_exr_part_colorspace(const Header &header);
|
|
|
|
/* XYZ with Illuminant E */
|
|
static Imf::Chromaticities CHROMATICITIES_XYZ_E{
|
|
{1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f}, {1.0f / 3.0f, 1.0f / 3.0f}};
|
|
/* Values matching ChromaticitiesForACES in https://github.com/ampas/aces_container */
|
|
static Imf::Chromaticities CHROMATICITIES_ACES_2065_1{
|
|
{0.7347f, 0.2653f}, {0.0f, 1.0f}, {0.0001f, -0.077f}, {0.32168f, 0.33767f}};
|
|
|
|
/* Memory Input Stream */
|
|
|
|
class IMemStream : public Imf::IStream {
|
|
public:
|
|
IMemStream(uchar *exrbuf, size_t exrsize) : IStream("<memory>"), _exrpos(0), _exrsize(exrsize)
|
|
{
|
|
_exrbuf = exrbuf;
|
|
}
|
|
|
|
bool read(char c[], int n) override
|
|
{
|
|
if (n + _exrpos <= _exrsize) {
|
|
memcpy(c, (void *)(&_exrbuf[_exrpos]), n);
|
|
_exrpos += n;
|
|
return true;
|
|
}
|
|
|
|
/* OpenEXR requests chunks of 4096 bytes even if the file is smaller than that. Return
|
|
* zeros when reading up to 2x that amount past the end of the file.
|
|
* This was fixed after the OpenEXR 3.3.2 release, but not in an official release yet. */
|
|
if (n + _exrpos < _exrsize + 8192) {
|
|
const size_t remainder = _exrsize - _exrpos;
|
|
if (remainder > 0) {
|
|
memcpy(c, (void *)(&_exrbuf[_exrpos]), remainder);
|
|
memset(c + remainder, 0, n - remainder);
|
|
_exrpos += n;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
exr_file_offset_t tellg() override
|
|
{
|
|
return _exrpos;
|
|
}
|
|
|
|
void seekg(exr_file_offset_t pos) override
|
|
{
|
|
_exrpos = pos;
|
|
}
|
|
|
|
void clear() override {}
|
|
|
|
private:
|
|
exr_file_offset_t _exrpos;
|
|
exr_file_offset_t _exrsize;
|
|
uchar *_exrbuf;
|
|
};
|
|
|
|
/* Memory-Mapped Input Stream */
|
|
|
|
class IMMapStream : public Imf::IStream {
|
|
public:
|
|
IMMapStream(const char *filepath) : IStream(filepath)
|
|
{
|
|
const int file = BLI_open(filepath, O_BINARY | O_RDONLY, 0);
|
|
if (file < 0) {
|
|
throw IEX_NAMESPACE::InputExc("file not found");
|
|
}
|
|
_exrpos = 0;
|
|
imb_mmap_lock();
|
|
_mmap_file = BLI_mmap_open(file);
|
|
imb_mmap_unlock();
|
|
close(file);
|
|
if (_mmap_file == nullptr) {
|
|
throw IEX_NAMESPACE::InputExc("BLI_mmap_open failed");
|
|
}
|
|
_exrbuf = (uchar *)BLI_mmap_get_pointer(_mmap_file);
|
|
_exrsize = BLI_mmap_get_length(_mmap_file);
|
|
}
|
|
|
|
~IMMapStream() override
|
|
{
|
|
imb_mmap_lock();
|
|
BLI_mmap_free(_mmap_file);
|
|
imb_mmap_unlock();
|
|
}
|
|
|
|
/* This is implementing regular `read`, not `readMemoryMapped`, because DWAA and DWAB
|
|
* decompressors load on unaligned offsets. Therefore we can't avoid the memory copy. */
|
|
|
|
bool read(char c[], int n) override
|
|
{
|
|
if (_exrpos + n > _exrsize) {
|
|
throw Iex::InputExc("Unexpected end of file.");
|
|
}
|
|
memcpy(c, _exrbuf + _exrpos, n);
|
|
_exrpos += n;
|
|
|
|
return _exrpos < _exrsize;
|
|
}
|
|
|
|
exr_file_offset_t tellg() override
|
|
{
|
|
return _exrpos;
|
|
}
|
|
|
|
void seekg(exr_file_offset_t pos) override
|
|
{
|
|
_exrpos = pos;
|
|
}
|
|
|
|
private:
|
|
BLI_mmap_file *_mmap_file;
|
|
exr_file_offset_t _exrpos;
|
|
exr_file_offset_t _exrsize;
|
|
uchar *_exrbuf;
|
|
};
|
|
|
|
/* File Input Stream */
|
|
|
|
class IFileStream : public Imf::IStream {
|
|
public:
|
|
IFileStream(const char *filepath) : IStream(filepath)
|
|
{
|
|
/* UTF8 file path support on windows. */
|
|
#if defined(WIN32)
|
|
wchar_t *wfilepath = alloc_utf16_from_8(filepath, 0);
|
|
ifs.open(wfilepath, std::ios_base::binary);
|
|
free(wfilepath);
|
|
#else
|
|
ifs.open(filepath, std::ios_base::binary);
|
|
#endif
|
|
|
|
if (!ifs) {
|
|
Iex::throwErrnoExc();
|
|
}
|
|
}
|
|
|
|
bool read(char c[], int n) override
|
|
{
|
|
if (!ifs) {
|
|
throw Iex::InputExc("Unexpected end of file.");
|
|
}
|
|
|
|
errno = 0;
|
|
ifs.read(c, n);
|
|
return check_error();
|
|
}
|
|
|
|
exr_file_offset_t tellg() override
|
|
{
|
|
return std::streamoff(ifs.tellg());
|
|
}
|
|
|
|
void seekg(exr_file_offset_t pos) override
|
|
{
|
|
ifs.seekg(pos);
|
|
check_error();
|
|
}
|
|
|
|
void clear() override
|
|
{
|
|
ifs.clear();
|
|
}
|
|
|
|
private:
|
|
bool check_error()
|
|
{
|
|
if (!ifs) {
|
|
if (errno) {
|
|
Iex::throwErrnoExc();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::ifstream ifs;
|
|
};
|
|
|
|
/* Memory Output Stream */
|
|
|
|
class OMemStream : public OStream {
|
|
public:
|
|
OMemStream(ImBuf *ibuf_) : OStream("<memory>"), ibuf(ibuf_), offset(0) {}
|
|
|
|
void write(const char c[], int n) override
|
|
{
|
|
ensure_size(offset + n);
|
|
memcpy(ibuf->encoded_buffer.data + offset, c, n);
|
|
offset += n;
|
|
ibuf->encoded_size += n;
|
|
}
|
|
|
|
exr_file_offset_t tellp() override
|
|
{
|
|
return offset;
|
|
}
|
|
|
|
void seekp(exr_file_offset_t pos) override
|
|
{
|
|
offset = pos;
|
|
ensure_size(offset);
|
|
}
|
|
|
|
private:
|
|
void ensure_size(exr_file_offset_t size)
|
|
{
|
|
/* if buffer is too small increase it. */
|
|
while (size > ibuf->encoded_buffer_size) {
|
|
if (!imb_enlargeencodedbufferImBuf(ibuf)) {
|
|
throw Iex::ErrnoExc("Out of memory.");
|
|
}
|
|
}
|
|
}
|
|
|
|
ImBuf *ibuf;
|
|
exr_file_offset_t offset;
|
|
};
|
|
|
|
/* File Output Stream */
|
|
|
|
class OFileStream : public OStream {
|
|
public:
|
|
OFileStream(const char *filepath) : OStream(filepath)
|
|
{
|
|
/* UTF8 file path support on windows. */
|
|
#if defined(WIN32)
|
|
wchar_t *wfilepath = alloc_utf16_from_8(filepath, 0);
|
|
ofs.open(wfilepath, std::ios_base::binary);
|
|
free(wfilepath);
|
|
#else
|
|
ofs.open(filepath, std::ios_base::binary);
|
|
#endif
|
|
|
|
if (!ofs) {
|
|
Iex::throwErrnoExc();
|
|
}
|
|
}
|
|
|
|
void write(const char c[], int n) override
|
|
{
|
|
errno = 0;
|
|
ofs.write(c, n);
|
|
check_error();
|
|
}
|
|
|
|
exr_file_offset_t tellp() override
|
|
{
|
|
return std::streamoff(ofs.tellp());
|
|
}
|
|
|
|
void seekp(exr_file_offset_t pos) override
|
|
{
|
|
ofs.seekp(pos);
|
|
check_error();
|
|
}
|
|
|
|
private:
|
|
void check_error()
|
|
{
|
|
if (!ofs) {
|
|
if (errno) {
|
|
Iex::throwErrnoExc();
|
|
}
|
|
|
|
throw Iex::ErrnoExc("File output failed.");
|
|
}
|
|
}
|
|
|
|
std::ofstream ofs;
|
|
};
|
|
|
|
struct _RGBAZ {
|
|
half r;
|
|
half g;
|
|
half b;
|
|
half a;
|
|
half z;
|
|
};
|
|
|
|
using RGBAZ = _RGBAZ;
|
|
|
|
static half float_to_half_safe(const float value)
|
|
{
|
|
return half(clamp_f(value, -HALF_MAX, HALF_MAX));
|
|
}
|
|
|
|
bool imb_is_a_openexr(const uchar *mem, const size_t size)
|
|
{
|
|
/* No define is exposed for this size. */
|
|
if (size < 4) {
|
|
return false;
|
|
}
|
|
return Imf::isImfMagic((const char *)mem);
|
|
}
|
|
|
|
static int openexr_jpg_like_quality_to_dwa_quality(int q)
|
|
{
|
|
q = blender::math::clamp(q, 0, 100);
|
|
|
|
/* Map default JPG quality of 90 to default DWA level of 45,
|
|
* "lossless" JPG quality of 100 to DWA level of 0, and everything else
|
|
* linearly based on those. */
|
|
constexpr int x0 = 100, y0 = 0;
|
|
constexpr int x1 = 90, y1 = 45;
|
|
q = y0 + (q - x0) * (y1 - y0) / (x1 - x0);
|
|
return q;
|
|
}
|
|
|
|
static void openexr_header_compression(Header *header, int compression, int quality)
|
|
{
|
|
switch (compression) {
|
|
case R_IMF_EXR_CODEC_NONE:
|
|
header->compression() = NO_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_PXR24:
|
|
header->compression() = PXR24_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_ZIP:
|
|
header->compression() = ZIP_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_PIZ:
|
|
header->compression() = PIZ_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_RLE:
|
|
header->compression() = RLE_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_ZIPS:
|
|
header->compression() = ZIPS_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_B44:
|
|
header->compression() = B44_COMPRESSION;
|
|
break;
|
|
case R_IMF_EXR_CODEC_B44A:
|
|
header->compression() = B44A_COMPRESSION;
|
|
break;
|
|
#if OPENEXR_VERSION_MAJOR > 2 || (OPENEXR_VERSION_MAJOR >= 2 && OPENEXR_VERSION_MINOR >= 2)
|
|
case R_IMF_EXR_CODEC_DWAA:
|
|
header->compression() = DWAA_COMPRESSION;
|
|
header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_quality(quality);
|
|
break;
|
|
case R_IMF_EXR_CODEC_DWAB:
|
|
header->compression() = DWAB_COMPRESSION;
|
|
header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_quality(quality);
|
|
break;
|
|
#endif
|
|
default:
|
|
header->compression() = ZIP_COMPRESSION;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int openexr_header_get_compression(const Header &header)
|
|
{
|
|
switch (header.compression()) {
|
|
case NO_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_NONE;
|
|
case RLE_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_RLE;
|
|
case ZIPS_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_ZIPS;
|
|
case ZIP_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_ZIP;
|
|
case PIZ_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_PIZ;
|
|
case PXR24_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_PXR24;
|
|
case B44_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_B44;
|
|
case B44A_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_B44A;
|
|
case DWAA_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_DWAA;
|
|
case DWAB_COMPRESSION:
|
|
return R_IMF_EXR_CODEC_DWAB;
|
|
case NUM_COMPRESSION_METHODS:
|
|
return R_IMF_EXR_CODEC_NONE;
|
|
}
|
|
return R_IMF_EXR_CODEC_NONE;
|
|
}
|
|
|
|
static void openexr_header_metadata_global(Header *header,
|
|
IDProperty *metadata,
|
|
const double ppm[2])
|
|
{
|
|
header->insert(
|
|
"Software",
|
|
TypedAttribute<std::string>(std::string("Blender ") + BKE_blender_version_string()));
|
|
|
|
if (metadata) {
|
|
LISTBASE_FOREACH (IDProperty *, prop, &metadata->data.group) {
|
|
/* Do not blindly pass along compression or colorInteropID, as they might have
|
|
* changed and will already be written when appropriate. */
|
|
if ((prop->type == IDP_STRING) && !STR_ELEM(prop->name, "compression", "colorInteropID")) {
|
|
header->insert(prop->name, StringAttribute(IDP_string_get(prop)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ppm[0] > 0.0 && ppm[1] > 0.0) {
|
|
/* Convert meters to inches. */
|
|
addXDensity(*header, ppm[0] * 0.0254);
|
|
header->pixelAspectRatio() = blender::math::safe_divide(ppm[1], ppm[0]);
|
|
}
|
|
}
|
|
|
|
static void openexr_header_metadata_colorspace(Header *header, const ColorSpace *colorspace)
|
|
{
|
|
if (colorspace == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const char *aces_colorspace = IMB_colormanagement_role_colorspace_name_get(
|
|
COLOR_ROLE_ACES_INTERCHANGE);
|
|
const char *ibuf_colorspace = IMB_colormanagement_colorspace_get_name(colorspace);
|
|
|
|
/* Write chromaticities for ACES-2065-1, as required by ACES container format. */
|
|
if (aces_colorspace && STREQ(aces_colorspace, ibuf_colorspace)) {
|
|
header->insert("chromaticities", TypedAttribute<Chromaticities>(CHROMATICITIES_ACES_2065_1));
|
|
header->insert("adoptedNeutral", TypedAttribute<V2f>(CHROMATICITIES_ACES_2065_1.white));
|
|
}
|
|
|
|
/* Write interop ID if available. */
|
|
blender::StringRefNull interop_id = IMB_colormanagement_space_get_interop_id(colorspace);
|
|
if (!interop_id.is_empty()) {
|
|
header->insert("colorInteropID", TypedAttribute<std::string>(interop_id));
|
|
}
|
|
}
|
|
|
|
static void openexr_header_metadata_colorspace(Header *header, ImBuf *ibuf)
|
|
{
|
|
/* Get colorspace from image buffer. */
|
|
const ColorSpace *colorspace = nullptr;
|
|
if (ibuf->float_buffer.data) {
|
|
colorspace = ibuf->float_buffer.colorspace;
|
|
if (colorspace == nullptr) {
|
|
colorspace = IMB_colormanagement_space_get_named(
|
|
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_SCENE_LINEAR));
|
|
}
|
|
}
|
|
else if (ibuf->byte_buffer.data) {
|
|
colorspace = ibuf->byte_buffer.colorspace;
|
|
}
|
|
|
|
openexr_header_metadata_colorspace(header, colorspace);
|
|
}
|
|
|
|
static void openexr_header_metadata_callback(void *data,
|
|
const char *propname,
|
|
char *prop,
|
|
int /*len*/)
|
|
{
|
|
Header *header = (Header *)data;
|
|
header->insert(propname, StringAttribute(prop));
|
|
}
|
|
|
|
static bool imb_save_openexr_half(ImBuf *ibuf, const char *filepath, const int flags)
|
|
{
|
|
const int channels = ibuf->channels;
|
|
const bool is_alpha = (channels >= 4) && (ibuf->planes == 32);
|
|
const int width = ibuf->x;
|
|
const int height = ibuf->y;
|
|
OStream *file_stream = nullptr;
|
|
|
|
try {
|
|
Header header(width, height);
|
|
|
|
openexr_header_compression(
|
|
&header, ibuf->foptions.flag & OPENEXR_CODEC_MASK, ibuf->foptions.quality);
|
|
openexr_header_metadata_global(&header, ibuf->metadata, ibuf->ppm);
|
|
openexr_header_metadata_colorspace(&header, ibuf);
|
|
|
|
/* create channels */
|
|
header.channels().insert("R", Channel(HALF));
|
|
header.channels().insert("G", Channel(HALF));
|
|
header.channels().insert("B", Channel(HALF));
|
|
if (is_alpha) {
|
|
header.channels().insert("A", Channel(HALF));
|
|
}
|
|
|
|
FrameBuffer frameBuffer;
|
|
|
|
/* Manually create `ofstream`, so we can handle UTF8 file-paths on windows. */
|
|
if (flags & IB_mem) {
|
|
file_stream = new OMemStream(ibuf);
|
|
}
|
|
else {
|
|
file_stream = new OFileStream(filepath);
|
|
}
|
|
OutputFile file(*file_stream, header);
|
|
|
|
/* we store first everything in half array */
|
|
std::unique_ptr<RGBAZ[]> pixels = std::unique_ptr<RGBAZ[]>(new RGBAZ[int64_t(height) * width]);
|
|
RGBAZ *to = pixels.get();
|
|
int xstride = sizeof(RGBAZ);
|
|
int ystride = xstride * width;
|
|
|
|
/* indicate used buffers */
|
|
frameBuffer.insert("R", Slice(HALF, (char *)&to->r, xstride, ystride));
|
|
frameBuffer.insert("G", Slice(HALF, (char *)&to->g, xstride, ystride));
|
|
frameBuffer.insert("B", Slice(HALF, (char *)&to->b, xstride, ystride));
|
|
if (is_alpha) {
|
|
frameBuffer.insert("A", Slice(HALF, (char *)&to->a, xstride, ystride));
|
|
}
|
|
if (ibuf->float_buffer.data) {
|
|
float *from;
|
|
|
|
for (int i = ibuf->y - 1; i >= 0; i--) {
|
|
from = ibuf->float_buffer.data + int64_t(channels) * i * width;
|
|
|
|
for (int j = ibuf->x; j > 0; j--) {
|
|
to->r = float_to_half_safe(from[0]);
|
|
to->g = float_to_half_safe((channels >= 2) ? from[1] : from[0]);
|
|
to->b = float_to_half_safe((channels >= 3) ? from[2] : from[0]);
|
|
to->a = float_to_half_safe((channels >= 4) ? from[3] : 1.0f);
|
|
to++;
|
|
from += channels;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
uchar *from;
|
|
|
|
for (int i = ibuf->y - 1; i >= 0; i--) {
|
|
from = ibuf->byte_buffer.data + int64_t(4) * i * width;
|
|
|
|
for (int j = ibuf->x; j > 0; j--) {
|
|
to->r = srgb_to_linearrgb(float(from[0]) / 255.0f);
|
|
to->g = srgb_to_linearrgb(float(from[1]) / 255.0f);
|
|
to->b = srgb_to_linearrgb(float(from[2]) / 255.0f);
|
|
to->a = channels >= 4 ? float(from[3]) / 255.0f : 1.0f;
|
|
to++;
|
|
from += 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
CLOG_DEBUG(&LOG, "Writing OpenEXR file of height %d", height);
|
|
|
|
file.setFrameBuffer(frameBuffer);
|
|
file.writePixels(height);
|
|
}
|
|
catch (const std::exception &exc) {
|
|
delete file_stream;
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
|
|
return false;
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
delete file_stream;
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
|
|
return false;
|
|
}
|
|
|
|
delete file_stream;
|
|
return true;
|
|
}
|
|
|
|
static bool imb_save_openexr_float(ImBuf *ibuf, const char *filepath, const int flags)
|
|
{
|
|
const int channels = ibuf->channels;
|
|
const bool is_alpha = (channels >= 4) && (ibuf->planes == 32);
|
|
const int width = ibuf->x;
|
|
const int height = ibuf->y;
|
|
OStream *file_stream = nullptr;
|
|
|
|
try {
|
|
Header header(width, height);
|
|
|
|
openexr_header_compression(
|
|
&header, ibuf->foptions.flag & OPENEXR_CODEC_MASK, ibuf->foptions.quality);
|
|
openexr_header_metadata_global(&header, ibuf->metadata, ibuf->ppm);
|
|
openexr_header_metadata_colorspace(&header, ibuf);
|
|
|
|
/* create channels */
|
|
header.channels().insert("R", Channel(Imf::FLOAT));
|
|
header.channels().insert("G", Channel(Imf::FLOAT));
|
|
header.channels().insert("B", Channel(Imf::FLOAT));
|
|
if (is_alpha) {
|
|
header.channels().insert("A", Channel(Imf::FLOAT));
|
|
}
|
|
|
|
FrameBuffer frameBuffer;
|
|
|
|
/* Manually create `ofstream`, so we can handle UTF8 file-paths on windows. */
|
|
if (flags & IB_mem) {
|
|
file_stream = new OMemStream(ibuf);
|
|
}
|
|
else {
|
|
file_stream = new OFileStream(filepath);
|
|
}
|
|
OutputFile file(*file_stream, header);
|
|
|
|
int xstride = sizeof(float) * channels;
|
|
int ystride = -xstride * width;
|
|
|
|
/* Last scan-line, stride negative. */
|
|
float *rect[4] = {nullptr, nullptr, nullptr, nullptr};
|
|
rect[0] = ibuf->float_buffer.data + int64_t(channels) * (height - 1) * width;
|
|
rect[1] = (channels >= 2) ? rect[0] + 1 : rect[0];
|
|
rect[2] = (channels >= 3) ? rect[0] + 2 : rect[0];
|
|
rect[3] = (channels >= 4) ?
|
|
rect[0] + 3 :
|
|
rect[0]; /* red as alpha, is this needed since alpha isn't written? */
|
|
|
|
frameBuffer.insert("R", Slice(Imf::FLOAT, (char *)rect[0], xstride, ystride));
|
|
frameBuffer.insert("G", Slice(Imf::FLOAT, (char *)rect[1], xstride, ystride));
|
|
frameBuffer.insert("B", Slice(Imf::FLOAT, (char *)rect[2], xstride, ystride));
|
|
if (is_alpha) {
|
|
frameBuffer.insert("A", Slice(Imf::FLOAT, (char *)rect[3], xstride, ystride));
|
|
}
|
|
|
|
file.setFrameBuffer(frameBuffer);
|
|
file.writePixels(height);
|
|
}
|
|
catch (const std::exception &exc) {
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
delete file_stream;
|
|
return false;
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
delete file_stream;
|
|
return false;
|
|
}
|
|
|
|
delete file_stream;
|
|
return true;
|
|
}
|
|
|
|
bool imb_save_openexr(ImBuf *ibuf, const char *filepath, int flags)
|
|
{
|
|
if (flags & IB_mem) {
|
|
imb_addencodedbufferImBuf(ibuf);
|
|
ibuf->encoded_size = 0;
|
|
}
|
|
|
|
if (ibuf->foptions.flag & OPENEXR_HALF) {
|
|
return imb_save_openexr_half(ibuf, filepath, flags);
|
|
}
|
|
|
|
/* when no float rect, we save as half (16 bits is sufficient) */
|
|
if (ibuf->float_buffer.data == nullptr) {
|
|
return imb_save_openexr_half(ibuf, filepath, flags);
|
|
}
|
|
|
|
return imb_save_openexr_float(ibuf, filepath, flags);
|
|
}
|
|
|
|
/* ******* Nicer API, MultiLayer and with Tile file support ************************************ */
|
|
|
|
/* naming rules:
|
|
* - parse name from right to left
|
|
* - last character is channel ID, 1 char like 'A' 'R' 'G' 'B' 'X' 'Y' 'Z' 'W' 'U' 'V'
|
|
* - separated with a dot; the Pass name (like "Depth", "Color", "Diffuse" or "Combined")
|
|
* - separated with a dot: the Layer name (like "Light1" or "Walls" or "Characters")
|
|
*/
|
|
|
|
/* flattened out channel */
|
|
struct ExrChannel {
|
|
/* Name and number of the part. */
|
|
std::string part_name;
|
|
int part_number = 0;
|
|
|
|
/* Full name of the chanel. */
|
|
std::string name;
|
|
/* Name as stored in the header. */
|
|
std::string internal_name;
|
|
/* Channel view. */
|
|
std::string view;
|
|
|
|
/* Colorspace. */
|
|
const ColorSpace *colorspace;
|
|
|
|
int xstride = 0, ystride = 0; /* step to next pixel, to next scan-line. */
|
|
float *rect = nullptr; /* first pointer to write in */
|
|
char chan_id = 0; /* quick lookup of channel char */
|
|
bool use_half_float = false; /* when saving use half float for file storage */
|
|
};
|
|
|
|
/* hierarchical; layers -> passes -> channels[] */
|
|
struct ExrPass {
|
|
~ExrPass()
|
|
{
|
|
if (rect) {
|
|
MEM_freeN(rect);
|
|
}
|
|
}
|
|
|
|
std::string name;
|
|
int totchan = 0;
|
|
float *rect = nullptr;
|
|
ExrChannel *chan[EXR_PASS_MAXCHAN] = {};
|
|
char chan_id[EXR_PASS_MAXCHAN] = {};
|
|
|
|
std::string internal_name; /* Name with no view. */
|
|
std::string view;
|
|
};
|
|
|
|
struct ExrLayer {
|
|
std::string name;
|
|
blender::Vector<ExrPass> passes;
|
|
};
|
|
|
|
struct ExrHandle {
|
|
std::string name;
|
|
|
|
IStream *ifile_stream = nullptr;
|
|
MultiPartInputFile *ifile = nullptr;
|
|
|
|
OFileStream *ofile_stream = nullptr;
|
|
MultiPartOutputFile *mpofile = nullptr;
|
|
OutputFile *ofile = nullptr;
|
|
|
|
bool write_multipart = false;
|
|
bool has_layer_pass_names = false;
|
|
|
|
int tilex = 0, tiley = 0;
|
|
int width = 0, height = 0;
|
|
int mipmap = 0;
|
|
|
|
StringVector views;
|
|
|
|
blender::Vector<ExrChannel> channels; /* flattened out channels. */
|
|
blender::Vector<ExrLayer> layers; /* layers and passes. */
|
|
};
|
|
|
|
static bool imb_exr_multilayer_parse_channels_from_file(ExrHandle *handle);
|
|
static blender::Vector<ExrChannel> exr_channels_in_multi_part_file(const MultiPartInputFile &file,
|
|
const bool parse_layers);
|
|
|
|
/* ********************** */
|
|
|
|
ExrHandle *IMB_exr_get_handle(const bool write_multipart)
|
|
{
|
|
ExrHandle *handle = MEM_new<ExrHandle>("ExrHandle");
|
|
handle->write_multipart = write_multipart;
|
|
return handle;
|
|
}
|
|
|
|
/* multiview functions */
|
|
|
|
void IMB_exr_add_view(ExrHandle *handle, const char *name)
|
|
{
|
|
handle->views.emplace_back(name);
|
|
}
|
|
|
|
static int imb_exr_get_multiView_id(StringVector &views, const std::string &name)
|
|
{
|
|
int count = 0;
|
|
for (StringVector::const_iterator i = views.begin(); count < views.size(); ++i) {
|
|
if (name == *i) {
|
|
return count;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
/* no views or wrong name */
|
|
return -1;
|
|
}
|
|
|
|
static StringVector imb_exr_get_views(MultiPartInputFile &file)
|
|
{
|
|
StringVector views;
|
|
|
|
for (int p = 0; p < file.parts(); p++) {
|
|
/* Views stored in separate parts. */
|
|
if (file.header(p).hasView()) {
|
|
const std::string &view = file.header(p).view();
|
|
if (imb_exr_get_multiView_id(views, view) == -1) {
|
|
views.push_back(view);
|
|
}
|
|
}
|
|
|
|
/* Part containing multiple views. */
|
|
if (hasMultiView(file.header(p))) {
|
|
StringVector multiview = multiView(file.header(p));
|
|
for (const std::string &view : multiview) {
|
|
if (imb_exr_get_multiView_id(views, view) == -1) {
|
|
views.push_back(view);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return views;
|
|
}
|
|
|
|
void IMB_exr_add_channels(ExrHandle *handle,
|
|
blender::StringRefNull layerpassname,
|
|
blender::StringRefNull channelnames,
|
|
blender::StringRefNull viewname,
|
|
blender::StringRefNull colorspace,
|
|
size_t xstride,
|
|
size_t ystride,
|
|
float *rect,
|
|
bool use_half_float)
|
|
{
|
|
/* For multipart, part name includes view since part names must be unique. */
|
|
std::string part_name;
|
|
if (handle->write_multipart) {
|
|
part_name = layerpassname;
|
|
if (!viewname.is_empty()) {
|
|
if (part_name.empty()) {
|
|
part_name = viewname;
|
|
}
|
|
else {
|
|
part_name = part_name + "-" + viewname;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If there are layer and pass names, we will write Blender multichannel metadata. */
|
|
if (!layerpassname.is_empty()) {
|
|
handle->has_layer_pass_names = true;
|
|
}
|
|
|
|
for (size_t channel = 0; channel < channelnames.size(); channel++) {
|
|
/* Full channel name including view (when not using multipart) and channel. */
|
|
std::string full_name = layerpassname;
|
|
if (!handle->write_multipart && !viewname.is_empty()) {
|
|
if (full_name.empty()) {
|
|
full_name = viewname;
|
|
}
|
|
else {
|
|
full_name = full_name + "." + viewname;
|
|
}
|
|
}
|
|
if (full_name.empty()) {
|
|
full_name = channelnames[channel];
|
|
}
|
|
else {
|
|
full_name = full_name + "." + channelnames[channel];
|
|
}
|
|
|
|
handle->channels.append_as();
|
|
ExrChannel &echan = handle->channels.last();
|
|
|
|
echan.name = full_name;
|
|
echan.internal_name = full_name;
|
|
echan.part_name = part_name;
|
|
echan.view = viewname;
|
|
echan.colorspace = IMB_colormanagement_space_get_named(colorspace.c_str());
|
|
|
|
echan.xstride = xstride;
|
|
echan.ystride = ystride;
|
|
echan.rect = rect + channel;
|
|
echan.use_half_float = use_half_float;
|
|
}
|
|
|
|
CLOG_DEBUG(&LOG, "Added pass %s %s", layerpassname.c_str(), channelnames.c_str());
|
|
}
|
|
|
|
static void openexr_header_metadata_multi(ExrHandle *handle,
|
|
Header &header,
|
|
const double ppm[2],
|
|
const StampData *stamp)
|
|
{
|
|
openexr_header_metadata_global(&header, nullptr, ppm);
|
|
if (handle->has_layer_pass_names) {
|
|
header.insert("BlenderMultiChannel", StringAttribute("Blender V2.55.1 and newer"));
|
|
}
|
|
if (!handle->write_multipart && !handle->views.empty() && !handle->views[0].empty()) {
|
|
addMultiView(header, handle->views);
|
|
}
|
|
BKE_stamp_info_callback(
|
|
&header, const_cast<StampData *>(stamp), openexr_header_metadata_callback, false);
|
|
}
|
|
|
|
bool IMB_exr_begin_write(ExrHandle *handle,
|
|
const char *filepath,
|
|
int width,
|
|
int height,
|
|
const double ppm[2],
|
|
int compress,
|
|
int quality,
|
|
const StampData *stamp)
|
|
{
|
|
if (handle->channels.is_empty()) {
|
|
CLOG_ERROR(&LOG, "Attempt to save MultiLayer without layers.");
|
|
return false;
|
|
}
|
|
|
|
Header header(width, height);
|
|
|
|
handle->width = width;
|
|
handle->height = height;
|
|
|
|
openexr_header_compression(&header, compress, quality);
|
|
|
|
if (!handle->write_multipart) {
|
|
/* If we're writing single part, we can only add one colorspace even if there are
|
|
* multiple passes with potentially different spaces. Prefer to write non-data
|
|
* colorspace in that case, since readers can detect data passes based on
|
|
* channels names being e.g. XYZ instead of RGB. */
|
|
bool found = false;
|
|
for (const ExrChannel &echan : handle->channels) {
|
|
if (echan.colorspace && !IMB_colormanagement_space_is_data(echan.colorspace)) {
|
|
openexr_header_metadata_colorspace(&header, echan.colorspace);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
if (const ColorSpace *colorspace = handle->channels[0].colorspace) {
|
|
openexr_header_metadata_colorspace(&header, colorspace);
|
|
}
|
|
}
|
|
}
|
|
|
|
blender::Vector<Header> part_headers;
|
|
|
|
blender::StringRefNull last_part_name;
|
|
|
|
for (const ExrChannel &echan : handle->channels) {
|
|
if (part_headers.is_empty() || last_part_name != echan.part_name) {
|
|
Header part_header = header;
|
|
|
|
/* When writing multipart, set name, view,type and colorspace in each part. */
|
|
if (handle->write_multipart) {
|
|
part_header.setName(echan.part_name);
|
|
if (!echan.view.empty()) {
|
|
part_header.insert("view", StringAttribute(echan.view));
|
|
}
|
|
part_header.insert("type", StringAttribute(SCANLINEIMAGE));
|
|
openexr_header_metadata_colorspace(&part_header, echan.colorspace);
|
|
}
|
|
|
|
/* Store global metadata in the first header only. Large metadata like cryptomatte would
|
|
* be bad to duplicate many times. */
|
|
if (part_headers.is_empty()) {
|
|
openexr_header_metadata_multi(handle, part_header, ppm, stamp);
|
|
}
|
|
|
|
part_headers.append(std::move(part_header));
|
|
last_part_name = echan.part_name;
|
|
}
|
|
|
|
part_headers.last().channels().insert(echan.name,
|
|
Channel(echan.use_half_float ? Imf::HALF : Imf::FLOAT));
|
|
}
|
|
|
|
BLI_assert(!(handle->write_multipart == false && part_headers.size() > 1));
|
|
|
|
/* Avoid crash/abort when we don't have permission to write here. */
|
|
/* Manually create `ofstream`, so we can handle UTF8 file-paths on windows. */
|
|
try {
|
|
handle->ofile_stream = new OFileStream(filepath);
|
|
if (handle->write_multipart) {
|
|
handle->mpofile = new MultiPartOutputFile(
|
|
*(handle->ofile_stream), part_headers.data(), part_headers.size());
|
|
}
|
|
else {
|
|
handle->ofile = new OutputFile(*(handle->ofile_stream), part_headers[0]);
|
|
}
|
|
}
|
|
catch (const std::exception &exc) {
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
|
|
delete handle->ofile;
|
|
delete handle->mpofile;
|
|
delete handle->ofile_stream;
|
|
|
|
handle->ofile = nullptr;
|
|
handle->mpofile = nullptr;
|
|
handle->ofile_stream = nullptr;
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
|
|
delete handle->ofile;
|
|
delete handle->mpofile;
|
|
delete handle->ofile_stream;
|
|
|
|
handle->ofile = nullptr;
|
|
handle->mpofile = nullptr;
|
|
handle->ofile_stream = nullptr;
|
|
}
|
|
|
|
return (handle->ofile != nullptr || handle->mpofile != nullptr);
|
|
}
|
|
|
|
bool IMB_exr_begin_read(
|
|
ExrHandle *handle, const char *filepath, int *width, int *height, const bool parse_channels)
|
|
{
|
|
/* 32 is arbitrary, but zero length files crashes exr. */
|
|
if (!(BLI_exists(filepath) && BLI_file_size(filepath) > 32)) {
|
|
return false;
|
|
}
|
|
|
|
/* avoid crash/abort when we don't have permission to write here */
|
|
try {
|
|
handle->ifile_stream = new IFileStream(filepath);
|
|
handle->ifile = new MultiPartInputFile(*(handle->ifile_stream));
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
delete handle->ifile;
|
|
delete handle->ifile_stream;
|
|
|
|
handle->ifile = nullptr;
|
|
handle->ifile_stream = nullptr;
|
|
}
|
|
|
|
if (!handle->ifile) {
|
|
return false;
|
|
}
|
|
|
|
Box2i dw = handle->ifile->header(0).dataWindow();
|
|
handle->width = *width = dw.max.x - dw.min.x + 1;
|
|
handle->height = *height = dw.max.y - dw.min.y + 1;
|
|
|
|
if (parse_channels) {
|
|
/* Parse channels into view/layer/pass. */
|
|
if (!imb_exr_multilayer_parse_channels_from_file(handle)) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
/* Read view and channels without parsing layers and passes. */
|
|
handle->views = imb_exr_get_views(*handle->ifile);
|
|
handle->channels = exr_channels_in_multi_part_file(*handle->ifile, false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IMB_exr_set_channel(
|
|
ExrHandle *handle, blender::StringRefNull full_name, int xstride, int ystride, float *rect)
|
|
{
|
|
for (ExrChannel &echan : handle->channels) {
|
|
if (echan.name == full_name) {
|
|
echan.xstride = xstride;
|
|
echan.ystride = ystride;
|
|
echan.rect = rect;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void IMB_exr_write_channels(ExrHandle *handle)
|
|
{
|
|
if (handle->channels.is_empty()) {
|
|
CLOG_ERROR(&LOG, "Attempt to save MultiLayer without layers.");
|
|
return;
|
|
}
|
|
|
|
const size_t num_pixels = size_t(handle->width) * handle->height;
|
|
const size_t num_parts = (handle->mpofile) ? handle->mpofile->parts() : 1;
|
|
|
|
for (size_t part_num = 0; part_num < num_parts; part_num++) {
|
|
const std::string &part_id = (handle->mpofile) ? handle->mpofile->header(part_num).name() : "";
|
|
/* We allocate temporary storage for half pixels for all the channels at once. */
|
|
int num_half_channels = 0;
|
|
for (const ExrChannel &echan : handle->channels) {
|
|
if (echan.part_name == part_id && echan.use_half_float) {
|
|
num_half_channels++;
|
|
}
|
|
}
|
|
|
|
blender::Vector<half> rect_half;
|
|
half *current_rect_half = nullptr;
|
|
if (num_half_channels > 0) {
|
|
rect_half.resize(size_t(num_half_channels) * num_pixels);
|
|
current_rect_half = rect_half.data();
|
|
}
|
|
|
|
FrameBuffer frameBuffer;
|
|
|
|
for (const ExrChannel &echan : handle->channels) {
|
|
/* Writing starts from last scan-line, stride negative. */
|
|
if (echan.part_name != part_id) {
|
|
continue;
|
|
}
|
|
|
|
if (echan.use_half_float) {
|
|
const float *rect = echan.rect;
|
|
half *cur = current_rect_half;
|
|
for (size_t i = 0; i < num_pixels; i++, cur++) {
|
|
*cur = float_to_half_safe(rect[i * echan.xstride]);
|
|
}
|
|
half *rect_to_write = current_rect_half + (handle->height - 1L) * handle->width;
|
|
frameBuffer.insert(
|
|
echan.name,
|
|
Slice(Imf::HALF, (char *)rect_to_write, sizeof(half), -handle->width * sizeof(half)));
|
|
current_rect_half += num_pixels;
|
|
}
|
|
else {
|
|
float *rect = echan.rect + echan.xstride * (handle->height - 1L) * handle->width;
|
|
frameBuffer.insert(echan.name,
|
|
Slice(Imf::FLOAT,
|
|
(char *)rect,
|
|
echan.xstride * sizeof(float),
|
|
-echan.ystride * sizeof(float)));
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (handle->mpofile) {
|
|
OutputPart part(*handle->mpofile, part_num);
|
|
part.setFrameBuffer(frameBuffer);
|
|
part.writePixels(handle->height);
|
|
}
|
|
else {
|
|
handle->ofile->setFrameBuffer(frameBuffer);
|
|
handle->ofile->writePixels(handle->height);
|
|
}
|
|
}
|
|
catch (const std::exception &exc) {
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
}
|
|
}
|
|
}
|
|
|
|
void IMB_exr_read_channels(ExrHandle *handle)
|
|
{
|
|
int numparts = handle->ifile->parts();
|
|
|
|
/* Check if EXR was saved with previous versions of blender which flipped images. */
|
|
const StringAttribute *ta = handle->ifile->header(0).findTypedAttribute<StringAttribute>(
|
|
"BlenderMultiChannel");
|
|
|
|
/* 'previous multilayer attribute, flipped. */
|
|
short flip = (ta && STRPREFIX(ta->value().c_str(), "Blender V2.43"));
|
|
|
|
CLOG_DEBUG(&LOG,
|
|
"\nIMB_exr_read_channels\n%s %-6s %-22s "
|
|
"\"%s\"\n---------------------------------------------------------------------",
|
|
"p",
|
|
"view",
|
|
"name",
|
|
"internal_name");
|
|
|
|
for (int i = 0; i < numparts; i++) {
|
|
/* Read part header. */
|
|
InputPart in(*handle->ifile, i);
|
|
Header header = in.header();
|
|
Box2i dw = header.dataWindow();
|
|
|
|
/* Insert all matching channel into frame-buffer. */
|
|
FrameBuffer frameBuffer;
|
|
|
|
for (const ExrChannel &echan : handle->channels) {
|
|
if (echan.part_number != i) {
|
|
continue;
|
|
}
|
|
|
|
CLOG_DEBUG(&LOG,
|
|
"%d %-6s %-22s \"%s\"\n",
|
|
echan.part_number,
|
|
echan.view.c_str(),
|
|
echan.name.c_str(),
|
|
echan.internal_name.c_str());
|
|
|
|
if (echan.rect) {
|
|
float *rect = echan.rect;
|
|
size_t xstride = echan.xstride * sizeof(float);
|
|
size_t ystride = echan.ystride * sizeof(float);
|
|
|
|
if (!flip) {
|
|
/* Inverse correct first pixel for data-window coordinates. */
|
|
rect -= echan.xstride * (dw.min.x - dw.min.y * handle->width);
|
|
/* Move to last scan-line to flip to Blender convention. */
|
|
rect += echan.xstride * (handle->height - 1) * handle->width;
|
|
ystride = -ystride;
|
|
}
|
|
else {
|
|
/* Inverse correct first pixel for data-window coordinates. */
|
|
rect -= echan.xstride * (dw.min.x + dw.min.y * handle->width);
|
|
}
|
|
|
|
frameBuffer.insert(echan.internal_name, Slice(Imf::FLOAT, (char *)rect, xstride, ystride));
|
|
}
|
|
}
|
|
|
|
/* Read pixels. */
|
|
try {
|
|
in.setFrameBuffer(frameBuffer);
|
|
CLOG_DEBUG(&LOG, "readPixels:readPixels[%d]: min.y: %d, max.y: %d", i, dw.min.y, dw.max.y);
|
|
in.readPixels(dw.min.y, dw.max.y);
|
|
}
|
|
catch (const std::exception &exc) {
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
break;
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void IMB_exr_multilayer_convert(ExrHandle *handle,
|
|
void *base,
|
|
void *(*addview)(void *base, const char *str),
|
|
void *(*addlayer)(void *base, const char *str),
|
|
void (*addpass)(void *base,
|
|
void *lay,
|
|
const char *str,
|
|
float *rect,
|
|
int totchan,
|
|
const char *chan_id,
|
|
const char *view))
|
|
{
|
|
/* RenderResult needs at least one RenderView */
|
|
if (handle->views.empty()) {
|
|
addview(base, "");
|
|
}
|
|
else {
|
|
/* add views to RenderResult */
|
|
for (const std::string &view_name : handle->views) {
|
|
addview(base, view_name.c_str());
|
|
}
|
|
}
|
|
|
|
if (handle->layers.is_empty()) {
|
|
CLOG_WARN(&LOG, "Cannot convert multilayer, no layers in handle");
|
|
return;
|
|
}
|
|
|
|
for (ExrLayer &lay : handle->layers) {
|
|
void *laybase = addlayer(base, lay.name.c_str());
|
|
if (laybase) {
|
|
for (ExrPass &pass : lay.passes) {
|
|
addpass(base,
|
|
laybase,
|
|
pass.internal_name.c_str(),
|
|
pass.rect,
|
|
pass.totchan,
|
|
pass.chan_id,
|
|
pass.view.c_str());
|
|
pass.rect = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void IMB_exr_close(ExrHandle *handle)
|
|
{
|
|
delete handle->ifile;
|
|
delete handle->ifile_stream;
|
|
delete handle->ofile;
|
|
delete handle->mpofile;
|
|
delete handle->ofile_stream;
|
|
|
|
MEM_delete(handle);
|
|
}
|
|
|
|
/* ********* */
|
|
|
|
/** Get a sub-string from the end of the name, separated by '.'. */
|
|
static int imb_exr_split_token(const char *str, const char *end, const char **token)
|
|
{
|
|
const char delims[] = {'.', '\0'};
|
|
const char *sep;
|
|
|
|
BLI_str_partition_ex(str, end, delims, &sep, token, true);
|
|
|
|
if (!sep) {
|
|
*token = str;
|
|
}
|
|
|
|
return int(end - *token);
|
|
}
|
|
|
|
static void imb_exr_pass_name_from_channel(char *passname,
|
|
const ExrChannel &echan,
|
|
const char *channelname,
|
|
const bool has_xyz_channels)
|
|
{
|
|
const int passname_maxncpy = EXR_TOT_MAXNAME;
|
|
|
|
if (echan.chan_id == 'Z' && (!has_xyz_channels || BLI_strcaseeq(channelname, "depth"))) {
|
|
BLI_strncpy(passname, "Depth", passname_maxncpy);
|
|
}
|
|
else if (echan.chan_id == 'Y' && !has_xyz_channels) {
|
|
BLI_strncpy(passname, channelname, passname_maxncpy);
|
|
}
|
|
else if (ELEM(echan.chan_id, 'R', 'G', 'B', 'A', 'V', 'X', 'Y', 'Z')) {
|
|
BLI_strncpy(passname, "Combined", passname_maxncpy);
|
|
}
|
|
else {
|
|
BLI_strncpy(passname, channelname, passname_maxncpy);
|
|
}
|
|
}
|
|
|
|
static void imb_exr_pass_name_from_channel_name(char *passname,
|
|
const ExrChannel & /*echan*/,
|
|
const char *channelname,
|
|
const bool /*has_xyz_channels*/)
|
|
{
|
|
const int passname_maxncpy = EXR_TOT_MAXNAME;
|
|
|
|
/* TODO: Are special tricks similar to imb_exr_pass_name_from_channel() needed here?
|
|
* Note that unknown passes are default to chan_id='X'. The place where this function is called
|
|
* is when the channel name is more than 1 character, so perhaps using just channel ID is not
|
|
* fully correct here. */
|
|
|
|
BLI_strncpy(passname, channelname, passname_maxncpy);
|
|
}
|
|
|
|
static int imb_exr_split_channel_name(ExrChannel &echan,
|
|
char *layname,
|
|
char *passname,
|
|
bool has_xyz_channels)
|
|
{
|
|
const int layname_maxncpy = EXR_TOT_MAXNAME;
|
|
const char *name = echan.name.c_str();
|
|
const char *end = name + strlen(name);
|
|
const char *token;
|
|
|
|
/* Some multi-layers have the combined buffer with names V, RGBA, or XYZ saved. Additionally, the
|
|
* Z channel can be interpreted as a Depth channel, but we only detect it as such if no X and Y
|
|
* channels exists, since the Z in this case is part of XYZ. The same goes for the Y channel,
|
|
* which can be detected as a luminance channel with the same name. */
|
|
if (name[1] == 0) {
|
|
/* Notice that we will be comparing with this upper-case version of the channel name, so the
|
|
* below comparisons are effectively not case sensitive, and would also consider lowercase
|
|
* versions of the listed channels. */
|
|
echan.chan_id = BLI_toupper_ascii(name[0]);
|
|
layname[0] = '\0';
|
|
imb_exr_pass_name_from_channel(passname, echan, name, has_xyz_channels);
|
|
return 1;
|
|
}
|
|
|
|
/* last token is channel identifier */
|
|
size_t len = imb_exr_split_token(name, end, &token);
|
|
if (len == 0) {
|
|
CLOG_ERROR(&LOG, "Multilayer read: bad channel name: %s", name);
|
|
return 0;
|
|
}
|
|
|
|
char channelname[EXR_TOT_MAXNAME];
|
|
BLI_strncpy(channelname, token, std::min(len + 1, sizeof(channelname)));
|
|
|
|
if (len == 1) {
|
|
echan.chan_id = BLI_toupper_ascii(channelname[0]);
|
|
}
|
|
else {
|
|
BLI_assert(len > 1); /* Checks above ensure. */
|
|
if (len == 2) {
|
|
/* Some multi-layers are using two-letter channels name,
|
|
* like, MX or NZ, which is basically has structure of
|
|
* <pass_prefix><component>
|
|
*
|
|
* This is a bit silly, but see file from #35658.
|
|
*
|
|
* Here we do some magic to distinguish such cases.
|
|
*/
|
|
const char chan_id = BLI_toupper_ascii(channelname[1]);
|
|
if (ELEM(chan_id, 'X', 'Y', 'Z', 'R', 'G', 'B', 'U', 'V', 'A')) {
|
|
echan.chan_id = chan_id;
|
|
}
|
|
else {
|
|
echan.chan_id = 'X'; /* Default to X if unknown. */
|
|
}
|
|
}
|
|
else if (BLI_strcaseeq(channelname, "red")) {
|
|
echan.chan_id = 'R';
|
|
}
|
|
else if (BLI_strcaseeq(channelname, "green")) {
|
|
echan.chan_id = 'G';
|
|
}
|
|
else if (BLI_strcaseeq(channelname, "blue")) {
|
|
echan.chan_id = 'B';
|
|
}
|
|
else if (BLI_strcaseeq(channelname, "alpha")) {
|
|
echan.chan_id = 'A';
|
|
}
|
|
else if (BLI_strcaseeq(channelname, "depth")) {
|
|
echan.chan_id = 'Z';
|
|
}
|
|
else {
|
|
echan.chan_id = 'X'; /* Default to X if unknown. */
|
|
}
|
|
}
|
|
end -= len + 1; /* +1 to skip '.' separator */
|
|
|
|
if (end > name) {
|
|
/* second token is pass name */
|
|
len = imb_exr_split_token(name, end, &token);
|
|
if (len == 0) {
|
|
CLOG_ERROR(&LOG, "Multilayer read: bad channel name: %s", name);
|
|
return 0;
|
|
}
|
|
BLI_strncpy(passname, token, len + 1);
|
|
end -= len + 1; /* +1 to skip '.' separator */
|
|
}
|
|
else {
|
|
/* Single token, determine pass name from channel name. */
|
|
imb_exr_pass_name_from_channel_name(passname, echan, channelname, has_xyz_channels);
|
|
}
|
|
|
|
/* all preceding tokens combined as layer name */
|
|
if (end > name) {
|
|
BLI_strncpy(layname, name, std::min(layname_maxncpy, int(end - name) + 1));
|
|
}
|
|
else {
|
|
layname[0] = '\0';
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static ExrLayer *imb_exr_get_layer(ExrHandle *handle, const char *layname)
|
|
{
|
|
for (ExrLayer &lay : handle->layers) {
|
|
if (lay.name == layname) {
|
|
return &lay;
|
|
}
|
|
}
|
|
|
|
handle->layers.append_as();
|
|
ExrLayer &lay = handle->layers.last();
|
|
lay.name = layname;
|
|
return &lay;
|
|
}
|
|
|
|
static ExrPass *imb_exr_get_pass(ExrLayer &lay, const char *passname)
|
|
{
|
|
for (ExrPass &pass : lay.passes) {
|
|
if (pass.name == passname) {
|
|
return &pass;
|
|
}
|
|
}
|
|
|
|
ExrPass pass;
|
|
pass.name = passname;
|
|
|
|
if (STREQ(passname, "Combined")) {
|
|
lay.passes.prepend(std::move(pass));
|
|
return &lay.passes.first();
|
|
}
|
|
|
|
lay.passes.append(std::move(pass));
|
|
return &lay.passes.last();
|
|
}
|
|
|
|
static bool exr_has_xyz_channels(ExrHandle *exr_handle)
|
|
{
|
|
bool x_found = false;
|
|
bool y_found = false;
|
|
bool z_found = false;
|
|
for (const ExrChannel &echan : exr_handle->channels) {
|
|
if (ELEM(echan.name, "X", "x")) {
|
|
x_found = true;
|
|
}
|
|
if (ELEM(echan.name, "Y", "y")) {
|
|
y_found = true;
|
|
}
|
|
if (ELEM(echan.name, "Z", "z")) {
|
|
z_found = true;
|
|
}
|
|
}
|
|
|
|
return x_found && y_found && z_found;
|
|
}
|
|
|
|
/* Replacement for OpenEXR GetChannelsInMultiPartFile, that also handles the
|
|
* case where parts are used for passes instead of multiview. */
|
|
static blender::Vector<ExrChannel> exr_channels_in_multi_part_file(const MultiPartInputFile &file,
|
|
const bool parse_layers)
|
|
{
|
|
blender::Vector<ExrChannel> channels;
|
|
const ColorSpace *global_colorspace = imb_exr_part_colorspace(file.header(0));
|
|
|
|
/* Get channels from each part. */
|
|
for (int p = 0; p < file.parts(); p++) {
|
|
const ChannelList &c = file.header(p).channels();
|
|
|
|
/* Parse colorspace. Per part colorspaces are not currently used, but
|
|
* might as well populate them them for consistency with writing. */
|
|
const ColorSpace *colorspace = imb_exr_part_colorspace(file.header(p));
|
|
if (colorspace == nullptr) {
|
|
colorspace = global_colorspace;
|
|
}
|
|
|
|
/* There are two ways of storing multiview EXRs:
|
|
* - Multiple views in part with multiView attribute.
|
|
* - Each view in its own part with view attribute. */
|
|
const bool has_multiple_views_in_part = hasMultiView(file.header(p));
|
|
StringVector views_in_part;
|
|
if (has_multiple_views_in_part) {
|
|
views_in_part = multiView(file.header(p));
|
|
}
|
|
blender::StringRef part_view;
|
|
if (file.header(p).hasView()) {
|
|
part_view = file.header(p).view();
|
|
}
|
|
|
|
/* Parse part name. */
|
|
blender::StringRef part_name;
|
|
if (parse_layers && file.header(p).hasName()) {
|
|
part_name = file.header(p).name();
|
|
|
|
/* Strip view name suffix if views are stored in separate parts.
|
|
* They need to be included to make the part names unique. */
|
|
if (!has_multiple_views_in_part) {
|
|
if (part_name.endswith("." + part_view)) {
|
|
part_name = part_name.drop_known_suffix("." + part_view);
|
|
}
|
|
else if (part_name.endswith("-" + part_view)) {
|
|
part_name = part_name.drop_known_suffix("-" + part_view);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Parse channels. */
|
|
for (ChannelList::ConstIterator i = c.begin(); i != c.end(); i++) {
|
|
ExrChannel echan;
|
|
echan.name = std::string(i.name());
|
|
echan.internal_name = echan.name;
|
|
|
|
if (has_multiple_views_in_part) {
|
|
echan.view = viewFromChannelName(echan.name, views_in_part);
|
|
echan.name = removeViewName(echan.internal_name, echan.view);
|
|
}
|
|
else {
|
|
echan.view = part_view;
|
|
}
|
|
|
|
if (parse_layers) {
|
|
/* Prepend part name as potential layer or pass name. According to OpenEXR docs
|
|
* this should not be needed, but Houdini writes files like this. */
|
|
if (!part_name.is_empty() && !blender::StringRef(echan.name).startswith(part_name + ".")) {
|
|
echan.name = part_name + "." + echan.name;
|
|
}
|
|
}
|
|
|
|
echan.part_number = p;
|
|
echan.colorspace = colorspace;
|
|
channels.append(std::move(echan));
|
|
}
|
|
}
|
|
|
|
return channels;
|
|
}
|
|
|
|
static bool imb_exr_multilayer_parse_channels_from_file(ExrHandle *handle)
|
|
{
|
|
handle->views = imb_exr_get_views(*handle->ifile);
|
|
handle->channels = exr_channels_in_multi_part_file(*handle->ifile, true);
|
|
|
|
const bool has_xyz_channels = exr_has_xyz_channels(handle);
|
|
|
|
/* now try to sort out how to assign memory to the channels */
|
|
/* first build hierarchical layer list */
|
|
for (ExrChannel &echan : handle->channels) {
|
|
char layname[EXR_TOT_MAXNAME], passname[EXR_TOT_MAXNAME];
|
|
if (imb_exr_split_channel_name(echan, layname, passname, has_xyz_channels)) {
|
|
const char *view = echan.view.c_str();
|
|
std::string internal_name = passname;
|
|
|
|
if (view[0] != '\0') {
|
|
char tmp_pass[EXR_PASS_MAXNAME];
|
|
SNPRINTF(tmp_pass, "%s.%s", passname, view);
|
|
STRNCPY(passname, tmp_pass);
|
|
}
|
|
|
|
ExrLayer *lay = imb_exr_get_layer(handle, layname);
|
|
ExrPass *pass = imb_exr_get_pass(*lay, passname);
|
|
|
|
pass->chan[pass->totchan] = &echan;
|
|
pass->totchan++;
|
|
pass->view = view;
|
|
pass->internal_name = internal_name;
|
|
|
|
if (pass->totchan >= EXR_PASS_MAXCHAN) {
|
|
CLOG_ERROR(&LOG, "Too many channels in one pass: %s", echan.name.c_str());
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* with some heuristics, try to merge the channels in buffers */
|
|
for (ExrLayer &lay : handle->layers) {
|
|
for (ExrPass &pass : lay.passes) {
|
|
if (pass.totchan) {
|
|
pass.rect = MEM_calloc_arrayN<float>(
|
|
size_t(handle->width) * size_t(handle->height) * size_t(pass.totchan), "pass rect");
|
|
if (pass.totchan == 1) {
|
|
ExrChannel &echan = *pass.chan[0];
|
|
echan.rect = pass.rect;
|
|
echan.xstride = 1;
|
|
echan.ystride = handle->width;
|
|
pass.chan_id[0] = echan.chan_id;
|
|
}
|
|
else {
|
|
char lookup[256];
|
|
|
|
memset(lookup, 0, sizeof(lookup));
|
|
|
|
/* we can have RGB(A), XYZ(W), UVA */
|
|
if (ELEM(pass.totchan, 3, 4)) {
|
|
if (pass.chan[0]->chan_id == 'B' || pass.chan[1]->chan_id == 'B' ||
|
|
pass.chan[2]->chan_id == 'B')
|
|
{
|
|
lookup[uint('R')] = 0;
|
|
lookup[uint('G')] = 1;
|
|
lookup[uint('B')] = 2;
|
|
lookup[uint('A')] = 3;
|
|
}
|
|
else if (pass.chan[0]->chan_id == 'Y' || pass.chan[1]->chan_id == 'Y' ||
|
|
pass.chan[2]->chan_id == 'Y')
|
|
{
|
|
lookup[uint('X')] = 0;
|
|
lookup[uint('Y')] = 1;
|
|
lookup[uint('Z')] = 2;
|
|
lookup[uint('W')] = 3;
|
|
}
|
|
else {
|
|
lookup[uint('U')] = 0;
|
|
lookup[uint('V')] = 1;
|
|
lookup[uint('A')] = 2;
|
|
}
|
|
for (int a = 0; a < pass.totchan; a++) {
|
|
ExrChannel &echan = *pass.chan[a];
|
|
echan.rect = pass.rect + lookup[uint(echan.chan_id)];
|
|
echan.xstride = pass.totchan;
|
|
echan.ystride = handle->width * pass.totchan;
|
|
pass.chan_id[uint(lookup[uint(echan.chan_id)])] = echan.chan_id;
|
|
}
|
|
}
|
|
else { /* unknown */
|
|
for (int a = 0; a < pass.totchan; a++) {
|
|
ExrChannel &echan = *pass.chan[a];
|
|
echan.rect = pass.rect + a;
|
|
echan.xstride = pass.totchan;
|
|
echan.ystride = handle->width * pass.totchan;
|
|
pass.chan_id[a] = echan.chan_id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* creates channels, makes a hierarchy and assigns memory to channels */
|
|
static ExrHandle *imb_exr_begin_read_mem(IStream &file_stream,
|
|
MultiPartInputFile &file,
|
|
int width,
|
|
int height)
|
|
{
|
|
ExrHandle *handle = IMB_exr_get_handle();
|
|
|
|
handle->ifile_stream = &file_stream;
|
|
handle->ifile = &file;
|
|
|
|
handle->width = width;
|
|
handle->height = height;
|
|
|
|
if (!imb_exr_multilayer_parse_channels_from_file(handle)) {
|
|
IMB_exr_close(handle);
|
|
return nullptr;
|
|
}
|
|
|
|
return handle;
|
|
}
|
|
|
|
/* ********************************************************* */
|
|
|
|
static void exr_print_filecontents(MultiPartInputFile &file)
|
|
{
|
|
int numparts = file.parts();
|
|
if (numparts == 1 && hasMultiView(file.header(0))) {
|
|
const StringVector views = multiView(file.header(0));
|
|
CLOG_DEBUG(&LOG, "MultiView file");
|
|
CLOG_DEBUG(&LOG, "Default view: %s", defaultViewName(views).c_str());
|
|
for (const std::string &view : views) {
|
|
CLOG_DEBUG(&LOG, "Found view %s", view.c_str());
|
|
}
|
|
}
|
|
else if (numparts > 1) {
|
|
CLOG_DEBUG(&LOG, "MultiPart file");
|
|
for (int i = 0; i < numparts; i++) {
|
|
if (file.header(i).hasView()) {
|
|
CLOG_DEBUG(&LOG, "Part %d: view = \"%s\"", i, file.header(i).view().c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < numparts; j++) {
|
|
const ChannelList &channels = file.header(j).channels();
|
|
for (ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i) {
|
|
const Channel &channel = i.channel();
|
|
CLOG_DEBUG(&LOG, "Found channel %s of type %d", i.name(), channel.type);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* For non-multi-layer, map R G B A channel names to something that's in this file. */
|
|
static const char *exr_rgba_channelname(MultiPartInputFile &file, const char *chan)
|
|
{
|
|
const ChannelList &channels = file.header(0).channels();
|
|
|
|
for (ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i) {
|
|
// const Channel &channel = i.channel(); /* Not used yet. */
|
|
const char *str = i.name();
|
|
int len = strlen(str);
|
|
if (len) {
|
|
if (BLI_strcasecmp(chan, str + len - 1) == 0) {
|
|
return str;
|
|
}
|
|
}
|
|
}
|
|
return chan;
|
|
}
|
|
|
|
static int exr_has_rgb(MultiPartInputFile &file, const char *rgb_channels[3])
|
|
{
|
|
/* Common names for RGB-like channels in order. The V channel name is used by convention for BW
|
|
* images, which will be broadcast to RGB channel at the end. */
|
|
static const char *channel_names[] = {
|
|
"V", "R", "Red", "G", "Green", "B", "Blue", "AR", "RA", "AG", "GA", "AB", "BA", nullptr};
|
|
|
|
const Header &header = file.header(0);
|
|
int num_channels = 0;
|
|
|
|
for (int i = 0; channel_names[i]; i++) {
|
|
/* Also try to match lower case variant of the channel names. */
|
|
std::string lower_case_name = std::string(channel_names[i]);
|
|
std::transform(lower_case_name.begin(),
|
|
lower_case_name.end(),
|
|
lower_case_name.begin(),
|
|
[](uchar c) { return std::tolower(c); });
|
|
|
|
if (header.channels().findChannel(channel_names[i]) ||
|
|
header.channels().findChannel(lower_case_name))
|
|
{
|
|
rgb_channels[num_channels++] = channel_names[i];
|
|
if (num_channels == 3) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return num_channels;
|
|
}
|
|
|
|
static bool exr_has_luma(MultiPartInputFile &file)
|
|
{
|
|
/* Y channel is the luma and should always present fir luma space images,
|
|
* optionally it could be also channels for chromas called BY and RY.
|
|
*/
|
|
const Header &header = file.header(0);
|
|
return header.channels().findChannel("Y") != nullptr;
|
|
}
|
|
|
|
static bool exr_has_chroma(MultiPartInputFile &file)
|
|
{
|
|
const Header &header = file.header(0);
|
|
return header.channels().findChannel("BY") != nullptr &&
|
|
header.channels().findChannel("RY") != nullptr;
|
|
}
|
|
|
|
static bool exr_has_alpha(MultiPartInputFile &file)
|
|
{
|
|
const Header &header = file.header(0);
|
|
return !(header.channels().findChannel("A") == nullptr);
|
|
}
|
|
|
|
static bool exr_has_xyz(MultiPartInputFile &file)
|
|
{
|
|
const Header &header = file.header(0);
|
|
return (header.channels().findChannel("X") != nullptr ||
|
|
header.channels().findChannel("x") != nullptr) &&
|
|
(header.channels().findChannel("Y") != nullptr ||
|
|
header.channels().findChannel("y") != nullptr) &&
|
|
(header.channels().findChannel("Z") != nullptr ||
|
|
header.channels().findChannel("z") != nullptr);
|
|
}
|
|
|
|
static bool exr_is_half_float(MultiPartInputFile &file)
|
|
{
|
|
const ChannelList &channels = file.header(0).channels();
|
|
for (ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i) {
|
|
const Channel &channel = i.channel();
|
|
if (channel.type != HALF) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool imb_exr_is_multilayer_file(MultiPartInputFile &file)
|
|
{
|
|
const ChannelList &channels = file.header(0).channels();
|
|
std::set<std::string> layerNames;
|
|
|
|
/* This will not include empty layer names, so files with just R/G/B/A
|
|
* channels without a layer name will be single layer. */
|
|
channels.layers(layerNames);
|
|
|
|
return !layerNames.empty();
|
|
}
|
|
|
|
static bool exr_has_multiview(MultiPartInputFile &file)
|
|
{
|
|
for (int p = 0; p < file.parts(); p++) {
|
|
if (hasMultiView(file.header(p))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool exr_has_multipart_file(MultiPartInputFile &file)
|
|
{
|
|
return file.parts() > 1;
|
|
}
|
|
|
|
/* it returns true if the file is multilayer or multiview */
|
|
static bool imb_exr_is_multi(MultiPartInputFile &file)
|
|
{
|
|
/* Multipart files are treated as multilayer in blender -
|
|
* even if they are single layer openexr with multiview. */
|
|
if (exr_has_multipart_file(file)) {
|
|
return true;
|
|
}
|
|
|
|
if (exr_has_multiview(file)) {
|
|
return true;
|
|
}
|
|
|
|
if (imb_exr_is_multilayer_file(file)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool IMB_exr_has_multilayer(ExrHandle *handle)
|
|
{
|
|
return imb_exr_is_multi(*handle->ifile);
|
|
}
|
|
|
|
static bool imb_check_chromaticity_val(float test_v, float ref_v)
|
|
{
|
|
const float tolerance_v = 0.000001f;
|
|
return (test_v < (ref_v + tolerance_v)) && (test_v > (ref_v - tolerance_v));
|
|
}
|
|
|
|
/* https://openexr.com/en/latest/TechnicalIntroduction.html#recommendations */
|
|
static bool imb_check_chromaticity_matches(const Imf::Chromaticities &a,
|
|
const Imf::Chromaticities &b)
|
|
{
|
|
return imb_check_chromaticity_val(a.red.x, b.red.x) &&
|
|
imb_check_chromaticity_val(a.red.y, b.red.y) &&
|
|
imb_check_chromaticity_val(a.green.x, b.green.x) &&
|
|
imb_check_chromaticity_val(a.green.y, b.green.y) &&
|
|
imb_check_chromaticity_val(a.blue.x, b.blue.x) &&
|
|
imb_check_chromaticity_val(a.blue.y, b.blue.y) &&
|
|
imb_check_chromaticity_val(a.white.x, b.white.x) &&
|
|
imb_check_chromaticity_val(a.white.y, b.white.y);
|
|
}
|
|
|
|
static void imb_exr_set_known_colorspace(const Header &header, ImFileColorSpace &r_colorspace)
|
|
{
|
|
r_colorspace.is_hdr_float = true;
|
|
|
|
/* Read ACES container format metadata. */
|
|
const IntAttribute *header_aces_container = header.findTypedAttribute<IntAttribute>(
|
|
"acesImageContainerFlag");
|
|
const ChromaticitiesAttribute *header_chromaticities =
|
|
header.findTypedAttribute<ChromaticitiesAttribute>("chromaticities");
|
|
|
|
if ((header_aces_container && header_aces_container->value() == 1) ||
|
|
(header_chromaticities &&
|
|
imb_check_chromaticity_matches(header_chromaticities->value(), CHROMATICITIES_ACES_2065_1)))
|
|
{
|
|
const char *known_colorspace = IMB_colormanagement_role_colorspace_name_get(
|
|
COLOR_ROLE_ACES_INTERCHANGE);
|
|
if (known_colorspace) {
|
|
STRNCPY_UTF8(r_colorspace.metadata_colorspace, known_colorspace);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const StringAttribute *header_interop_id = header.findTypedAttribute<StringAttribute>(
|
|
"colorInteropID");
|
|
|
|
/* Next try interop ID. */
|
|
if (header_interop_id && !header_interop_id->value().empty()) {
|
|
const ColorSpace *colorspace = IMB_colormanagement_space_from_interop_id(
|
|
header_interop_id->value());
|
|
if (colorspace) {
|
|
STRNCPY_UTF8(r_colorspace.metadata_colorspace,
|
|
IMB_colormanagement_colorspace_get_name(colorspace));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Try chromaticities. */
|
|
if (header_chromaticities &&
|
|
(imb_check_chromaticity_matches(header_chromaticities->value(), CHROMATICITIES_XYZ_E)))
|
|
{
|
|
/* Only works for the Blender default configuration due to fixed name. */
|
|
STRNCPY_UTF8(r_colorspace.metadata_colorspace, "Linear CIE-XYZ E");
|
|
}
|
|
}
|
|
|
|
static const ColorSpace *imb_exr_part_colorspace(const Header &header)
|
|
{
|
|
ImFileColorSpace colorspace;
|
|
imb_exr_set_known_colorspace(header, colorspace);
|
|
return IMB_colormanagement_space_get_named(colorspace.metadata_colorspace);
|
|
}
|
|
|
|
static bool exr_get_ppm(MultiPartInputFile &file, double ppm[2])
|
|
{
|
|
const Header &header = file.header(0);
|
|
if (!hasXDensity(header)) {
|
|
return false;
|
|
}
|
|
ppm[0] = double(xDensity(header)) / 0.0254;
|
|
ppm[1] = ppm[0] * double(header.pixelAspectRatio());
|
|
return true;
|
|
}
|
|
|
|
bool IMB_exr_get_ppm(ExrHandle *handle, double ppm[2])
|
|
{
|
|
return exr_get_ppm(*handle->ifile, ppm);
|
|
}
|
|
|
|
ImBuf *imb_load_openexr(const uchar *mem, size_t size, int flags, ImFileColorSpace &r_colorspace)
|
|
{
|
|
ImBuf *ibuf = nullptr;
|
|
IMemStream *membuf = nullptr;
|
|
MultiPartInputFile *file = nullptr;
|
|
|
|
if (imb_is_a_openexr(mem, size) == 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
try {
|
|
bool is_multi;
|
|
|
|
membuf = new IMemStream((uchar *)mem, size);
|
|
file = new MultiPartInputFile(*membuf);
|
|
|
|
const Header &file_header = file->header(0);
|
|
Box2i dw = file_header.dataWindow();
|
|
const size_t width = dw.max.x - dw.min.x + 1;
|
|
const size_t height = dw.max.y - dw.min.y + 1;
|
|
|
|
CLOG_DEBUG(&LOG, "Image data window %d %d %d %d", dw.min.x, dw.min.y, dw.max.x, dw.max.y);
|
|
|
|
if (CLOG_CHECK(&LOG, CLG_LEVEL_DEBUG)) {
|
|
exr_print_filecontents(*file);
|
|
}
|
|
|
|
is_multi = imb_exr_is_multi(*file);
|
|
|
|
/* do not make an ibuf when */
|
|
if (is_multi && !(flags & IB_test) && !(flags & IB_multilayer)) {
|
|
CLOG_ERROR(&LOG, "Cannot process EXR multilayer file");
|
|
}
|
|
else {
|
|
const bool is_alpha = exr_has_alpha(*file);
|
|
|
|
ibuf = IMB_allocImBuf(width, height, is_alpha ? 32 : 24, 0);
|
|
ibuf->foptions.flag |= exr_is_half_float(*file) ? OPENEXR_HALF : 0;
|
|
ibuf->foptions.flag |= openexr_header_get_compression(file_header);
|
|
|
|
exr_get_ppm(*file, ibuf->ppm);
|
|
|
|
imb_exr_set_known_colorspace(file_header, r_colorspace);
|
|
|
|
ibuf->ftype = IMB_FTYPE_OPENEXR;
|
|
|
|
if (!(flags & IB_test)) {
|
|
|
|
if (flags & IB_metadata) {
|
|
Header::ConstIterator iter;
|
|
|
|
IMB_metadata_ensure(&ibuf->metadata);
|
|
for (iter = file_header.begin(); iter != file_header.end(); iter++) {
|
|
const StringAttribute *attr = file_header.findTypedAttribute<StringAttribute>(
|
|
iter.name());
|
|
|
|
/* not all attributes are string attributes so we might get some NULLs here */
|
|
if (attr) {
|
|
IMB_metadata_set_field(ibuf->metadata, iter.name(), attr->value().c_str());
|
|
ibuf->flags |= IB_metadata;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Only enters with IB_multilayer flag set. */
|
|
if (is_multi && ((flags & IB_thumbnail) == 0)) {
|
|
/* constructs channels for reading, allocates memory in channels */
|
|
ExrHandle *handle = imb_exr_begin_read_mem(*membuf, *file, width, height);
|
|
if (handle) {
|
|
IMB_exr_read_channels(handle);
|
|
ibuf->exrhandle = handle; /* potential danger, the caller has to check for this! */
|
|
}
|
|
}
|
|
else {
|
|
const char *rgb_channels[3];
|
|
const int num_rgb_channels = exr_has_rgb(*file, rgb_channels);
|
|
const bool has_luma = exr_has_luma(*file);
|
|
const bool has_xyz = exr_has_xyz(*file);
|
|
FrameBuffer frameBuffer;
|
|
float *first;
|
|
size_t xstride = sizeof(float[4]);
|
|
size_t ystride = -xstride * width;
|
|
|
|
/* No need to clear image memory, it will be fully written below. */
|
|
IMB_alloc_float_pixels(ibuf, 4, false);
|
|
|
|
/* Inverse correct first pixel for data-window
|
|
* coordinates (- dw.min.y because of y flip). */
|
|
first = ibuf->float_buffer.data - 4 * (dw.min.x - dw.min.y * width);
|
|
/* But, since we read y-flipped (negative y stride) we move to last scan-line. */
|
|
first += 4 * (height - 1) * width;
|
|
|
|
if (num_rgb_channels > 0) {
|
|
for (int i = 0; i < num_rgb_channels; i++) {
|
|
frameBuffer.insert(exr_rgba_channelname(*file, rgb_channels[i]),
|
|
Slice(Imf::FLOAT, (char *)(first + i), xstride, ystride));
|
|
}
|
|
}
|
|
else if (has_xyz) {
|
|
frameBuffer.insert(exr_rgba_channelname(*file, "X"),
|
|
Slice(Imf::FLOAT, (char *)first, xstride, ystride));
|
|
frameBuffer.insert(exr_rgba_channelname(*file, "Y"),
|
|
Slice(Imf::FLOAT, (char *)(first + 1), xstride, ystride));
|
|
frameBuffer.insert(exr_rgba_channelname(*file, "Z"),
|
|
Slice(Imf::FLOAT, (char *)(first + 2), xstride, ystride));
|
|
}
|
|
else if (has_luma) {
|
|
frameBuffer.insert(exr_rgba_channelname(*file, "Y"),
|
|
Slice(Imf::FLOAT, (char *)first, xstride, ystride));
|
|
frameBuffer.insert(
|
|
exr_rgba_channelname(*file, "BY"),
|
|
Slice(Imf::FLOAT, (char *)(first + 1), xstride, ystride, 1, 1, 0.5f));
|
|
frameBuffer.insert(
|
|
exr_rgba_channelname(*file, "RY"),
|
|
Slice(Imf::FLOAT, (char *)(first + 2), xstride, ystride, 1, 1, 0.5f));
|
|
}
|
|
|
|
/* 1.0 is fill value, this still needs to be assigned even when (is_alpha == 0) */
|
|
frameBuffer.insert(exr_rgba_channelname(*file, "A"),
|
|
Slice(Imf::FLOAT, (char *)(first + 3), xstride, ystride, 1, 1, 1.0f));
|
|
|
|
InputPart in(*file, 0);
|
|
in.setFrameBuffer(frameBuffer);
|
|
in.readPixels(dw.min.y, dw.max.y);
|
|
|
|
/* XXX, ImBuf has no nice way to deal with this.
|
|
* ideally IM_rect would be used when the caller wants a rect BUT
|
|
* at the moment all functions use IM_rect.
|
|
* Disabling this is ok because all functions should check
|
|
* if a rect exists and create one on demand.
|
|
*
|
|
* Disabling this because the sequencer frees immediate. */
|
|
#if 0
|
|
if (flag & IM_rect) {
|
|
IMB_byte_from_float(ibuf);
|
|
}
|
|
#endif
|
|
|
|
if (num_rgb_channels == 0 && has_luma && exr_has_chroma(*file)) {
|
|
for (size_t a = 0; a < size_t(ibuf->x) * ibuf->y; a++) {
|
|
float *color = ibuf->float_buffer.data + a * 4;
|
|
ycc_to_rgb(color[0] * 255.0f,
|
|
color[1] * 255.0f,
|
|
color[2] * 255.0f,
|
|
&color[0],
|
|
&color[1],
|
|
&color[2],
|
|
BLI_YCC_ITU_BT709);
|
|
}
|
|
}
|
|
else if (!has_xyz && num_rgb_channels <= 1) {
|
|
/* Convert 1 to 3 channels. */
|
|
for (size_t a = 0; a < size_t(ibuf->x) * ibuf->y; a++) {
|
|
float *color = ibuf->float_buffer.data + a * 4;
|
|
color[1] = color[0];
|
|
color[2] = color[0];
|
|
}
|
|
}
|
|
|
|
/* file is no longer needed */
|
|
delete membuf;
|
|
delete file;
|
|
}
|
|
}
|
|
else {
|
|
delete membuf;
|
|
delete file;
|
|
}
|
|
|
|
if (flags & IB_alphamode_detect) {
|
|
ibuf->flags |= IB_alphamode_premul;
|
|
}
|
|
}
|
|
return ibuf;
|
|
}
|
|
catch (const std::exception &exc) {
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
if (ibuf) {
|
|
IMB_freeImBuf(ibuf);
|
|
}
|
|
delete file;
|
|
delete membuf;
|
|
|
|
return nullptr;
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
if (ibuf) {
|
|
IMB_freeImBuf(ibuf);
|
|
}
|
|
delete file;
|
|
delete membuf;
|
|
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
ImBuf *imb_load_filepath_thumbnail_openexr(const char *filepath,
|
|
const int /*flags*/,
|
|
const size_t max_thumb_size,
|
|
ImFileColorSpace &r_colorspace,
|
|
size_t *r_width,
|
|
size_t *r_height)
|
|
{
|
|
ImBuf *ibuf = nullptr;
|
|
IStream *stream = nullptr;
|
|
Imf::RgbaInputFile *file = nullptr;
|
|
|
|
/* OpenExr uses exceptions for error-handling. */
|
|
try {
|
|
|
|
/* The memory-mapped stream is faster, but don't use for huge files as it requires contiguous
|
|
* address space and we are processing multiple files at once (typically one per processor
|
|
* core). The 100 MB limit here is arbitrary, but seems reasonable and conservative. */
|
|
if (BLI_file_size(filepath) < 100 * 1024 * 1024) {
|
|
stream = new IMMapStream(filepath);
|
|
}
|
|
else {
|
|
stream = new IFileStream(filepath);
|
|
}
|
|
|
|
/* imb_initopenexr() creates a global pool of worker threads. But we thumbnail multiple images
|
|
* at once, and by default each file will attempt to use the entire pool for itself, stalling
|
|
* the others. So each thumbnail should use a single thread of the pool. */
|
|
file = new RgbaInputFile(*stream, 1);
|
|
|
|
if (!file->isComplete()) {
|
|
delete file;
|
|
delete stream;
|
|
return nullptr;
|
|
}
|
|
|
|
Imath::Box2i dw = file->dataWindow();
|
|
int source_w = dw.max.x - dw.min.x + 1;
|
|
int source_h = dw.max.y - dw.min.y + 1;
|
|
*r_width = source_w;
|
|
*r_height = source_h;
|
|
|
|
const Header &file_header = file->header();
|
|
|
|
/* If there is an embedded thumbnail, return that instead of making a new one. */
|
|
if (file_header.hasPreviewImage()) {
|
|
const Imf::PreviewImage &preview = file->header().previewImage();
|
|
ImBuf *ibuf = IMB_allocFromBuffer(
|
|
(uint8_t *)preview.pixels(), nullptr, preview.width(), preview.height(), 4);
|
|
delete file;
|
|
delete stream;
|
|
IMB_flipy(ibuf);
|
|
return ibuf;
|
|
}
|
|
|
|
/* No effect yet for thumbnails, but will work once it is supported. */
|
|
imb_exr_set_known_colorspace(file_header, r_colorspace);
|
|
|
|
/* Create a new thumbnail. */
|
|
float scale_factor = std::min(float(max_thumb_size) / float(source_w),
|
|
float(max_thumb_size) / float(source_h));
|
|
int dest_w = std::max(int(source_w * scale_factor), 1);
|
|
int dest_h = std::max(int(source_h * scale_factor), 1);
|
|
|
|
ibuf = IMB_allocImBuf(dest_w, dest_h, 32, IB_float_data);
|
|
|
|
/* A single row of source pixels. */
|
|
Imf::Array<Imf::Rgba> pixels(source_w);
|
|
|
|
/* Loop through destination thumbnail rows. */
|
|
for (int h = 0; h < dest_h; h++) {
|
|
|
|
/* Load the single source row that corresponds with destination row. */
|
|
int source_y = int(float(h) / scale_factor) + dw.min.y;
|
|
file->setFrameBuffer(&pixels[0] - dw.min.x - source_y * source_w, 1, source_w);
|
|
file->readPixels(source_y);
|
|
|
|
for (int w = 0; w < dest_w; w++) {
|
|
/* For each destination pixel find single corresponding source pixel. */
|
|
int source_x = int(std::min<int>((w / scale_factor), dw.max.x - 1));
|
|
float *dest_px = &ibuf->float_buffer.data[(h * dest_w + w) * 4];
|
|
dest_px[0] = pixels[source_x].r;
|
|
dest_px[1] = pixels[source_x].g;
|
|
dest_px[2] = pixels[source_x].b;
|
|
dest_px[3] = pixels[source_x].a;
|
|
}
|
|
}
|
|
|
|
if (file->lineOrder() == INCREASING_Y) {
|
|
IMB_flipy(ibuf);
|
|
}
|
|
|
|
delete file;
|
|
delete stream;
|
|
|
|
return ibuf;
|
|
}
|
|
|
|
catch (const std::exception &exc) {
|
|
CLOG_ERROR(&LOG, "%s: %s", __func__, exc.what());
|
|
if (ibuf) {
|
|
IMB_freeImBuf(ibuf);
|
|
}
|
|
|
|
delete file;
|
|
delete stream;
|
|
return nullptr;
|
|
}
|
|
catch (...) { /* Catch-all for edge cases or compiler bugs. */
|
|
CLOG_ERROR(&LOG, "Unknown error in %s", __func__);
|
|
if (ibuf) {
|
|
IMB_freeImBuf(ibuf);
|
|
}
|
|
|
|
delete file;
|
|
delete stream;
|
|
return nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void imb_initopenexr()
|
|
{
|
|
/* In a multithreaded program, staticInitialize() must be called once during startup, before the
|
|
* program accesses any other functions or classes in the IlmImf library. */
|
|
Imf::staticInitialize();
|
|
Imf::setGlobalThreadCount(BLI_system_thread_count());
|
|
}
|
|
|
|
void imb_exitopenexr()
|
|
{
|
|
/* Tells OpenEXR to free thread pool, also ensures there is no running tasks. */
|
|
Imf::setGlobalThreadCount(0);
|
|
}
|