/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * 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 . */ #include "config.h" #include #include /* These libpika includes are not needed here at all, but this is a * convenient place to make sure the public libpika headers are * C++-clean. The C++ compiler will choke on stuff like naming * a struct member or parameter "private". */ #include "libpika/pika.h" #include "libpika/pikaui.h" #include "libpikabase/pikabase.h" #include "libpikamath/pikamath.h" #include "libpikacolor/pikacolor.h" #include "libpikaconfig/pikaconfig.h" #include "libpikamodule/pikamodule.h" #include "libpikathumb/pikathumb.h" #include "libpikawidgets/pikawidgets.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 #include #include #include #include #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), data_window_(file_.header().dataWindow()), channels_(file_.header().channels()) { const Channel* chan; if (channels_.findChannel("R") || channels_.findChannel("G") || channels_.findChannel("B")) { format_string_ = "RGB"; image_type_ = IMAGE_TYPE_RGB; if ((chan = channels_.findChannel("R"))) pt_ = chan->type; else if ((chan = channels_.findChannel("G"))) pt_ = chan->type; else pt_ = channels_.findChannel("B")->type; } else if (channels_.findChannel("Y") && (channels_.findChannel("RY") || channels_.findChannel("BY"))) { format_string_ = "RGB"; image_type_ = IMAGE_TYPE_RGB; pt_ = channels_.findChannel("Y")->type; // FIXME: no chroma handling for now. throw; } else if (channels_.findChannel("Y")) { format_string_ = "Y"; image_type_ = IMAGE_TYPE_GRAY; pt_ = channels_.findChannel("Y")->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 { throw; } } if (channels_.findChannel("A")) { format_string_.append("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; } } int readPixelRow(char *pixels, int bpp, int row) { const int actual_row = data_window_.min.y + row; FrameBuffer fb; // 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); 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_GRAY: fb.insert("Y", Slice(pt_, base, bpp, 0, 1, 1, 0.5)); if (hasAlpha()) { fb.insert("A", Slice(pt_, base + bpc_, bpp, 0, 1, 1, 1.0)); } break; case IMAGE_TYPE_RGB: default: fb.insert("R", Slice(pt_, base + (bpc_ * 0), bpp, 0, 1, 1, 0.0)); fb.insert("G", Slice(pt_, base + (bpc_ * 1), bpp, 0, 1, 1, 0.0)); fb.insert("B", Slice(pt_, base + (bpc_ * 2), bpp, 0, 1, 1, 0.0)); if (hasAlpha()) { fb.insert("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; } PikaColorProfile *getProfile() const { Chromaticities chromaticities; float whiteLuminance = 1.0; PikaColorProfile *linear_srgb_profile; cmsHPROFILE linear_srgb_lcms; PikaColorProfile *profile; cmsHPROFILE lcms_profile; cmsCIEXYZ *pika_r_XYZ, *pika_g_XYZ, *pika_b_XYZ, *pika_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 PIKA's internal linear sRGB profile linear_srgb_profile = pika_color_profile_new_rgb_srgb_linear (); linear_srgb_lcms = pika_color_profile_get_lcms_profile (linear_srgb_profile); pika_r_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigRedColorantTag); pika_g_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigGreenColorantTag); pika_b_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigBlueColorantTag); pika_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 PIKA's internal profile bool exr_is_linear_srgb = XYZ_equal (&exr_r_XYZ, pika_r_XYZ) && XYZ_equal (&exr_g_XYZ, pika_g_XYZ) && XYZ_equal (&exr_b_XYZ, pika_b_XYZ) && XYZ_equal (&exr_w_XYZ, pika_w_XYZ); // using PIKA'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 libpikacolor/pikacolorprofile.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", "(PIKA 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 = pika_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("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("exif"); if (exif) { exif_data = (guchar *)(exif->value().data.get()); *size = exif->value().size; // darktable appends a jpg-compatible exif00 string, so get rid of that again: if ( ! memcmp (jpeg_exif, exif_data, sizeof(jpeg_exif))) { *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("xmp"); if (xmp) { *size = xmp->value().size(); result = (guchar *) g_memdup2 (xmp->value().data(), *size); } return result; } size_t refcount_; InputFile file_; const Box2i data_window_; const ChannelList& channels_; PixelType pt_; int bpc_; EXRImageType image_type_; bool has_alpha_; 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); } 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(); } PikaColorProfile * 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); } int exr_loader_read_pixel_row (EXRLoader *loader, char *pixels, int bpp, int row) { int retval = -1; // Don't let any exceptions propagate to the C layer. try { retval = loader->readPixelRow(pixels, bpp, row); } catch (...) { retval = -1; } return retval; }