Gimp/plug-ins/file-exr/openexr-wrapper.cc
2026-04-01 09:47:56 -03:00

728 lines
20 KiB
C++

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <string>
/* These libgimp includes are not needed here at all, but this is a
* convenient place to make sure the public libgimp headers are
* C++-clean. The C++ compiler will choke on stuff like naming
* a struct member or parameter "private".
*/
#include "libgimp/gimp.h"
#include "libgimp/gimpui.h"
#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpconfig/gimpconfig.h"
#include "libgimpmodule/gimpmodule.h"
#include "libgimpthumb/gimpthumb.h"
#include "libgimpwidgets/gimpwidgets.h"
#if defined(__MINGW32__)
#ifndef FLT_EPSILON
#define FLT_EPSILON __FLT_EPSILON__
#endif
#ifndef DBL_EPSILON
#define DBL_EPSILON __DBL_EPSILON__
#endif
#ifndef LDBL_EPSILON
#define LDBL_EPSILON __LDBL_EPSILON__
#endif
#endif
/* ignore deprecated warnings from OpenEXR headers */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated"
#include <lcms2.h>
#include <ImfInputFile.h>
#include <ImfChannelList.h>
#include <ImfRgbaFile.h>
#include <ImfArray.h>
#include <ImfRgbaYca.h>
#include <ImfStandardAttributes.h>
#pragma GCC diagnostic pop
#include "exr-attribute-blob.h"
#include "openexr-wrapper.h"
using namespace Imf;
using namespace Imf::RgbaYca;
using namespace Imath;
static bool XYZ_equal (cmsCIEXYZ *a, cmsCIEXYZ *b)
{
static const double epsilon = 0.0001;
/* Y is encoding the luminance, we normalize that for comparison */
return fabs ((a->X / a->Y * b->Y) - b->X) < epsilon &&
fabs ((a->Y / a->Y * b->Y) - b->Y) < epsilon &&
fabs ((a->Z / a->Y * b->Y) - b->Z) < epsilon;
}
struct _EXRLoader
{
_EXRLoader (const char* filename) :
refcount_ (1),
file_ (filename),
rgbaInputFile_ (filename),
data_window_ (file_.header ().dataWindow ()),
channels_ (file_.header ().channels ())
{
std::set<string> layerNames;
bool loaded;
channels_.layers (layerNames);
layers_only_ = false;
layer_count_ = layerNames.size ();
loaded = initializeImage (false);
/* The OpenEXR image may have named layers only, not loose channels.
* In that case, we need to go through each layer name and append it
* to the call for findChannel () */
if (! loaded)
{
layers_only_ = true;
for (std::set<std::string>::const_iterator i = layerNames.begin ();
i != layerNames.end (); ++i)
{
loaded = initializeImage (true, *i + ".");
if (loaded)
break;
}
}
}
bool initializeImage (bool has_layers,
const std::string &prefix = "")
{
const Channel* chan;
can_load_ = true;
/* Capitalization matters - channel "R" and "r" are both red,
* but unless you use the specific one mentioned in the file,
* it won't load. */
if (channels_.findChannel (prefix + "R") ||
channels_.findChannel (prefix + "G") ||
channels_.findChannel (prefix + "B") ||
channels_.findChannel (prefix + "r") ||
channels_.findChannel (prefix + "g") ||
channels_.findChannel (prefix + "b"))
{
format_string_ = "RGB";
image_type_ = IMAGE_TYPE_RGB;
if ((chan = channels_.findChannel (prefix + "R")) ||
(chan = channels_.findChannel (prefix + "r")))
pt_ = chan->type;
else if ((chan = channels_.findChannel (prefix + "G")) ||
(chan = channels_.findChannel (prefix + "g")))
pt_ = chan->type;
else if ((chan = channels_.findChannel (prefix + "B")) ||
(chan = channels_.findChannel (prefix + "b")))
pt_ = chan->type;
}
else if (channels_.findChannel (prefix + "Y") &&
(channels_.findChannel (prefix + "RY") ||
channels_.findChannel (prefix + "BY")))
{
format_string_ = "RGB";
image_type_ = IMAGE_TYPE_YUV;
pt_ = channels_.findChannel (prefix + "Y")->type;
}
else if (channels_.findChannel (prefix + "Y") ||
channels_.findChannel (prefix + "y"))
{
format_string_ = "Y";
image_type_ = IMAGE_TYPE_GRAY;
if ((chan = channels_.findChannel (prefix + "Y")) ||
(chan = channels_.findChannel (prefix + "y")))
pt_ = chan->type;
}
else
{
int channel_count = 0;
const char *channel_name = NULL;
for (ChannelList::ConstIterator i = channels_.begin ();
i != channels_.end (); ++i)
{
channel_count++;
pt_ = i.channel ().type;
channel_name = i.name ();
}
/* Assume single channel images are grayscale,
* no matter what the channel name is. */
if (channel_count == 1)
{
format_string_ = channel_name;
image_type_ = IMAGE_TYPE_UNKNOWN_1_CHANNEL;
unknown_channel_name_ = channel_name;
/* TODO: Pass this information back so it can be displayed
* in the UI. */
printf ("OpenEXR Warning: Single channel image with unknown "
"channel %s, loading as grayscale\n", channel_name);
}
else
{
/* TODO: After string freeze ends add unsupported type notice. */
can_load_ = false;
}
}
if (channels_.findChannel (prefix + "A"))
{
format_string_.append (prefix + "A");
has_alpha_ = true;
}
else if (channels_.findChannel (prefix + "a"))
{
format_string_.append (prefix + "a");
has_alpha_ = true;
}
else
{
has_alpha_ = false;
}
switch (pt_)
{
case UINT:
format_string_.append (" u32");
bpc_ = 4;
break;
case HALF:
format_string_.append (" half");
bpc_ = 2;
break;
case FLOAT:
default:
format_string_.append (" float");
bpc_ = 4;
}
return can_load_;
}
int readPixelRow (char *pixels,
int bpp,
int row,
int layer_index)
{
const int actual_row = data_window_.min.y + row;
FrameBuffer fb;
std::set<string> layerNames;
std::string prefix = "";
/* This is necessary because OpenEXR expects the buffer to begin at
* (0, 0). Though it probably results in some unmapped address,
* hopefully OpenEXR will not make use of it. :/ */
char* base = pixels - (data_window_.min.x * bpp);
if (layer_index > -1)
{
channels_.layers (layerNames);
prefix = *std::next (layerNames.begin (), layer_index) + ".";
}
switch (image_type_)
{
case IMAGE_TYPE_UNKNOWN_1_CHANNEL:
fb.insert (unknown_channel_name_, Slice (pt_, base, bpp, 0, 1, 1, 0.5));
break;
case IMAGE_TYPE_YUV:
{
int width = getWidth();
Array<Rgba> row_buffer (width);
rgbaInputFile_.setFrameBuffer (&row_buffer[0] - data_window_.min.x,
1,
0);
rgbaInputFile_.readPixels (actual_row, actual_row);
for (int i = 0; i < width; i++)
{
half *pixel = (half *)(base + (i * bpp));
pixel[0] = row_buffer[i].r;
pixel[1] = row_buffer[i].g;
pixel[2] = row_buffer[i].b;
if (hasAlpha ())
{
pixel[3] = row_buffer[i].a;
}
}
/* return early to skip the file_.setFrameBuffer() and
* file_.readPixels() calls below, which are used by the
* RGB and GRAY cases.
* YUV reading is handled entirely by rgbaInputFile_ above. */
return 0;
}
case IMAGE_TYPE_GRAY:
if (channels_.findChannel (prefix + "Y"))
{
fb.insert (prefix + "Y", Slice (pt_, base, bpp, 0, 1, 1, 0.5));
if (hasAlpha ())
{
fb.insert (prefix + "A", Slice (pt_, base + bpc_, bpp, 0, 1, 1, 1.0));
}
}
else
{
{
fb.insert (prefix + "y", Slice (pt_, base, bpp, 0, 1, 1, 0.5));
if (hasAlpha())
{
fb.insert (prefix + "a", Slice (pt_, base + bpc_, bpp, 0, 1, 1, 1.0));
}
}
}
break;
case IMAGE_TYPE_RGB:
default:
if (channels_.findChannel (prefix + "R"))
{
fb.insert (prefix + "R", Slice (pt_, base + (bpc_ * 0), bpp, 0, 1, 1, 0.0));
fb.insert (prefix + "G", Slice (pt_, base + (bpc_ * 1), bpp, 0, 1, 1, 0.0));
fb.insert (prefix + "B", Slice (pt_, base + (bpc_ * 2), bpp, 0, 1, 1, 0.0));
if (hasAlpha ())
{
fb.insert (prefix + "A", Slice (pt_, base + (bpc_ * 3), bpp, 0, 1, 1, 1.0));
}
}
else
{
fb.insert (prefix + "r", Slice (pt_, base + (bpc_ * 0), bpp, 0, 1, 1, 0.0));
fb.insert (prefix + "g", Slice (pt_, base + (bpc_ * 1), bpp, 0, 1, 1, 0.0));
fb.insert (prefix + "b", Slice (pt_, base + (bpc_ * 2), bpp, 0, 1, 1, 0.0));
if (hasAlpha ())
{
fb.insert (prefix + "a", Slice (pt_, base + (bpc_ * 3), bpp, 0, 1, 1, 1.0));
}
}
}
file_.setFrameBuffer (fb);
file_.readPixels (actual_row);
return 0;
}
int getWidth () const {
return data_window_.max.x - data_window_.min.x + 1;
}
int getHeight () const {
return data_window_.max.y - data_window_.min.y + 1;
}
EXRPrecision getPrecision () const {
EXRPrecision prec;
switch (pt_)
{
case UINT:
prec = PREC_UINT;
break;
case HALF:
prec = PREC_HALF;
break;
case FLOAT:
default:
prec = PREC_FLOAT;
}
return prec;
}
EXRImageType getImageType () const {
return image_type_;
}
int hasAlpha () const {
return has_alpha_ ? 1 : 0;
}
int canLoad () const {
return can_load_ ? 1 : 0;
}
GimpColorProfile *getProfile () const {
Chromaticities chromaticities;
float whiteLuminance = 1.0;
GimpColorProfile *linear_srgb_profile;
cmsHPROFILE linear_srgb_lcms;
GimpColorProfile *profile;
cmsHPROFILE lcms_profile;
cmsCIEXYZ *gimp_r_XYZ, *gimp_g_XYZ, *gimp_b_XYZ, *gimp_w_XYZ;
cmsCIEXYZ exr_r_XYZ, exr_g_XYZ, exr_b_XYZ, exr_w_XYZ;
/* get the color information from the EXR */
if (hasChromaticities (file_.header ()))
chromaticities = Imf::chromaticities (file_.header ());
else
return NULL;
if (Imf::hasWhiteLuminance (file_.header ()))
whiteLuminance = Imf::whiteLuminance (file_.header ());
else
return NULL;
#if 0
std::cout << "hasChromaticities: "
<< hasChromaticities (file_.header ())
<< std::endl;
std::cout << "hasWhiteLuminance: "
<< hasWhiteLuminance (file_.header ())
<< std::endl;
std::cout << whiteLuminance << std::endl;
std::cout << chromaticities.red << std::endl;
std::cout << chromaticities.green << std::endl;
std::cout << chromaticities.blue << std::endl;
std::cout << chromaticities.white << std::endl;
std::cout << std::endl;
#endif
cmsCIExyY whitePoint = { chromaticities.white.x,
chromaticities.white.y,
whiteLuminance };
cmsCIExyYTRIPLE CameraPrimaries = { { chromaticities.red.x,
chromaticities.red.y,
whiteLuminance },
{ chromaticities.green.x,
chromaticities.green.y,
whiteLuminance },
{ chromaticities.blue.x,
chromaticities.blue.y,
whiteLuminance } };
/* get the primaries + wp from GIMP's internal linear sRGB profile */
linear_srgb_profile = gimp_color_profile_new_rgb_srgb_linear ();
linear_srgb_lcms = gimp_color_profile_get_lcms_profile (linear_srgb_profile);
gimp_r_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigRedColorantTag);
gimp_g_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigGreenColorantTag);
gimp_b_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigBlueColorantTag);
gimp_w_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigMediaWhitePointTag);
cmsxyY2XYZ (&exr_r_XYZ, &CameraPrimaries.Red);
cmsxyY2XYZ (&exr_g_XYZ, &CameraPrimaries.Green);
cmsxyY2XYZ (&exr_b_XYZ, &CameraPrimaries.Blue);
cmsxyY2XYZ (&exr_w_XYZ, &whitePoint);
/* ... and check if the data stored in the EXR matches GIMP's internal profile */
bool exr_is_linear_srgb = XYZ_equal (&exr_r_XYZ, gimp_r_XYZ) &&
XYZ_equal (&exr_g_XYZ, gimp_g_XYZ) &&
XYZ_equal (&exr_b_XYZ, gimp_b_XYZ) &&
XYZ_equal (&exr_w_XYZ, gimp_w_XYZ);
/* using GIMP's linear sRGB profile allows to skip the conversion popup */
if (exr_is_linear_srgb)
return linear_srgb_profile;
/* nope, it's something else. Clean up and build a new profile */
g_object_unref (linear_srgb_profile);
/* TODO: maybe factor this out into libgimpcolor/gimpcolorprofile.h ? */
double Parameters[2] = { 1.0, 0.0 };
cmsToneCurve *Gamma[3];
Gamma[0] = Gamma[1] = Gamma[2] = cmsBuildParametricToneCurve(0,
1,
Parameters);
lcms_profile = cmsCreateRGBProfile (&whitePoint, &CameraPrimaries, Gamma);
cmsFreeToneCurve (Gamma[0]);
if (lcms_profile == NULL) return NULL;
/* cmsSetProfileVersion (lcms_profile, 2.1); */
cmsMLU *mlu0 = cmsMLUalloc (NULL, 1);
cmsMLUsetASCII (mlu0, "en", "US", "(GIMP internal)");
cmsMLU *mlu1 = cmsMLUalloc(NULL, 1);
cmsMLUsetASCII (mlu1, "en", "US", "color profile from EXR chromaticities");
cmsMLU *mlu2 = cmsMLUalloc(NULL, 1);
cmsMLUsetASCII (mlu2, "en", "US", "color profile from EXR chromaticities");
cmsWriteTag (lcms_profile, cmsSigDeviceMfgDescTag, mlu0);
cmsWriteTag (lcms_profile, cmsSigDeviceModelDescTag, mlu1);
cmsWriteTag (lcms_profile, cmsSigProfileDescriptionTag, mlu2);
cmsMLUfree (mlu0);
cmsMLUfree (mlu1);
cmsMLUfree (mlu2);
profile = gimp_color_profile_new_from_lcms_profile (lcms_profile,
NULL);
cmsCloseProfile (lcms_profile);
return profile;
}
gchar *getComment () const {
char *result = NULL;
const Imf::StringAttribute *comment = file_.header ().findTypedAttribute<Imf::StringAttribute>("comment");
if (comment)
result = g_strdup (comment->value().c_str());
return result;
}
guchar *getExif (guint *size) const {
guchar jpeg_exif[] = "Exif\0\0";
guchar *exif_data = NULL;
*size = 0;
const Imf::BlobAttribute *exif = file_.header ().findTypedAttribute<Imf::BlobAttribute>("exif");
if (exif)
{
exif_data = (guchar *)(exif->value ().data.get ());
*size = exif->value ().size;
/* darktable 4.0.0 and earlier appended a jpg-compatible exif00 string,
* so get rid of that again. We explicitly reduce the size by 1 since
* the compiler adds an extra \0 to the end of jpeg_exif. */
if ( ! memcmp (jpeg_exif, exif_data, sizeof (jpeg_exif)-1))
{
*size -= 6;
exif_data += 6;
}
}
return (guchar *)g_memdup2 (exif_data, *size);
}
guchar *getXmp (guint *size) const {
guchar *result = NULL;
*size = 0;
const Imf::StringAttribute *xmp = file_.header ().findTypedAttribute<Imf::StringAttribute>("xmp");
if (xmp)
{
*size = xmp->value ().size ();
result = (guchar *) g_memdup2 (xmp->value().data(), *size);
}
return result;
}
gchar *getLayerName (int index) const {
gchar *result = NULL;
if (index > -1 && index < layer_count_)
{
std::set<string> layerNames;
std::string name;
channels_.layers (layerNames);
name = *std::next (layerNames.begin(), index);
result = (gchar *) g_memdup2 (name.c_str(),
name.size() + 1);
}
return result;
}
int getLayerInfo (gint *num_layers, int *layers_only) const {
*num_layers = layer_count_;
*layers_only = layers_only_;
return 1;
}
size_t refcount_;
InputFile file_;
RgbaInputFile rgbaInputFile_;
const Box2i data_window_;
const ChannelList& channels_;
PixelType pt_;
int bpc_;
EXRImageType image_type_;
bool has_alpha_;
bool layers_only_;
int layer_count_;
bool can_load_;
std::string format_string_;
std::string unknown_channel_name_;
};
EXRLoader*
exr_loader_new (const char *filename)
{
EXRLoader* file;
/* Don't let any exceptions propagate to the C layer. */
try
{
Imf::BlobAttribute::registerAttributeType();
file = new EXRLoader (filename);
if (file && ! file->canLoad ())
{
exr_loader_unref (file);
file = NULL;
}
}
catch (...)
{
file = NULL;
}
return file;
}
EXRLoader*
exr_loader_ref (EXRLoader *loader)
{
++loader->refcount_;
return loader;
}
void
exr_loader_unref (EXRLoader *loader)
{
if (--loader->refcount_ == 0)
{
delete loader;
}
}
int
exr_loader_get_width (EXRLoader *loader)
{
int width;
/* Don't let any exceptions propagate to the C layer. */
try
{
width = loader->getWidth ();
}
catch (...)
{
width = -1;
}
return width;
}
int
exr_loader_get_height (EXRLoader *loader)
{
int height;
/* Don't let any exceptions propagate to the C layer. */
try
{
height = loader->getHeight ();
}
catch (...)
{
height = -1;
}
return height;
}
EXRImageType
exr_loader_get_image_type (EXRLoader *loader)
{
/* This does not throw. */
return loader->getImageType ();
}
EXRPrecision
exr_loader_get_precision (EXRLoader *loader)
{
/* This does not throw. */
return loader->getPrecision ();
}
int
exr_loader_has_alpha (EXRLoader *loader)
{
/* This does not throw. */
return loader->hasAlpha ();
}
GimpColorProfile *
exr_loader_get_profile (EXRLoader *loader)
{
return loader->getProfile ();
}
gchar *
exr_loader_get_comment (EXRLoader *loader)
{
return loader->getComment ();
}
guchar *
exr_loader_get_exif (EXRLoader *loader,
guint *size)
{
return loader->getExif (size);
}
guchar *
exr_loader_get_xmp (EXRLoader *loader,
guint *size)
{
return loader->getXmp (size);
}
gchar *
exr_loader_get_layer_name (EXRLoader *loader,
gint index)
{
return loader->getLayerName (index);
}
int
exr_loader_get_layer_info (EXRLoader *loader,
gint *num_layers,
gboolean *layers_only)
{
return loader->getLayerInfo (num_layers, layers_only);
}
int
exr_loader_read_pixel_row (EXRLoader *loader,
char *pixels,
int bpp,
int row,
int layer_index)
{
int retval = -1;
/* Don't let any exceptions propagate to the C layer. */
try
{
retval = loader->readPixelRow (pixels, bpp, row, layer_index);
}
catch (...)
{
retval = -1;
}
return retval;
}