/* This file is part of darktable, Copyright (C) 2010-2026 darktable developers. darktable 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. darktable 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 darktable. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "common/colorspaces.h" #include "common/darktable.h" #include "common/exif.h" #include "common/metadata.h" #include "common/datetime.h" #include "develop/develop.h" #include "imageio/imageio_exr.h" #include "imageio/imageio_exr.hh" dt_imageio_retval_t dt_imageio_open_exr(dt_image_t *img, const char *filename, dt_mipmap_buffer_t *mbuf) { bool isTiled = false; Imf::setGlobalThreadCount(dt_get_num_threads()); std::unique_ptr fileTiled; std::unique_ptr file; Imath::Box2i dw; Imf::FrameBuffer frameBuffer; int dx, dy; uint32_t xstride, ystride; /* verify openexr image */ if(!Imf::isOpenExrFile((const char *)filename, isTiled)) return DT_IMAGEIO_UNSUPPORTED_FORMAT; /* open exr file */ try { if(isTiled) { std::unique_ptr temp(new Imf::TiledInputFile(filename)); fileTiled = std::move(temp); } else { std::unique_ptr temp(new Imf::InputFile(filename)); file = std::move(temp); } } catch(const std::exception &e) { return DT_IMAGEIO_LOAD_FAILED; } const Imf::Header &header = isTiled ? fileTiled->header() : file->header(); /* check that channels available is any of supported RGB(a) */ bool hasR = false; bool hasG = false; bool hasB = false; for(Imf::ChannelList::ConstIterator i = header.channels().begin(); i != header.channels().end(); ++i) { std::string name(i.name()); if(name == "R") hasR = true; if(name == "G") hasG = true; if(name == "B") hasB = true; } if(!(hasR && hasG && hasB)) { dt_print(DT_DEBUG_ALWAYS, "[exr_open] error: only images with RGB(A) channels are supported," " skipping '%s'", img->filename); return DT_IMAGEIO_UNSUPPORTED_FEATURE; } if(!img->exif_inited) { // read back exif data, either as embedded blob or "standard" attributes // if another software is able to update these exif data, the former test // should be removed to take the potential changes in account (not done // by normal import image flow) const Imf::BlobAttribute *exif = header.findTypedAttribute("exif"); if(exif) { uint8_t *exif_blob = exif->value().data.get(); uint32_t exif_size = exif->value().size; /* skip any superfluous "Exif\0\0" APP1 prefix written by dt 4.0.0 and earlier */ if(exif_size >= 6 && !memcmp(exif_blob, "Exif\0\0", 6)) { exif_blob += 6; exif_size -= 6; } if(exif_size > 0) dt_exif_read_from_blob(img, exif_blob, exif_size); } else { if(Imf::hasOwner(header)) { dt_metadata_set_import_lock(img->id, "Xmp.dc.rights", Imf::owner(header).c_str()); } if(Imf::hasComments(header)) { dt_metadata_set_import_lock(img->id, "Xmp.dc.description", Imf::comments(header).c_str()); } if(Imf::hasCapDate(header)) { // utcOffset can be ignored for now, see dt_datetime_exif_to_numbers() char *datetime = strdup(Imf::capDate(header).c_str()); dt_datetime_exif_to_img(img, datetime); free(datetime); } if(Imf::hasLongitude(header) && Imf::hasLatitude(header)) { img->geoloc.longitude = Imf::longitude(header); img->geoloc.latitude = Imf::latitude(header); } if(Imf::hasAltitude(header)) img->geoloc.elevation = Imf::altitude(header); if(Imf::hasFocus(header)) img->exif_focus_distance = Imf::focus(header); if(Imf::hasExpTime(header)) img->exif_exposure = Imf::expTime(header); if(Imf::hasAperture(header)) img->exif_aperture = Imf::aperture(header); if(Imf::hasIsoSpeed(header)) img->exif_iso = Imf::isoSpeed(header); // the OPENEXR_VERSION_HEX macro is broken for 3.2.0 and earlier, must compute directly #if(((OPENEXR_VERSION_MAJOR << 24) | (OPENEXR_VERSION_MINOR << 16) | (OPENEXR_VERSION_PATCH << 8)) >= 0x03020000) if(Imf::hasCameraMake(header)) { g_strlcpy(img->exif_maker, Imf::cameraMake(header).c_str(), sizeof(img->exif_maker)); } if(Imf::hasCameraModel(header)) { g_strlcpy(img->exif_model, Imf::cameraModel(header).c_str(), sizeof(img->exif_model)); } // Make sure we copy the exif make and model to the correct place if needed dt_image_refresh_makermodel(img); if(Imf::hasLensModel(header)) { g_strlcpy(img->exif_lens, Imf::lensModel(header).c_str(), sizeof(img->exif_lens)); if(Imf::hasLensMake(header) && Imf::lensMake(header) == "Canon") { /* Use pretty name for Canon RF & RF-S lenses (as exiftool/exiv2/lensfun) */ if(g_str_has_prefix(img->exif_lens, "RF")) { std::string pretty; if(img->exif_lens[2] == '-') pretty = "Canon RF-S " + std::string(&img->exif_lens[4]); else pretty = "Canon RF " + std::string(&img->exif_lens[2]); g_strlcpy(img->exif_lens, pretty.c_str(), sizeof(img->exif_lens)); } } // Capitalize lenses properly for UI presentation, e.g. // Nikon Z-mount: Nikkor, Tamron // Leica L-mount: Viltrox if(g_str_has_prefix(img->exif_lens, "NIKKOR ") || g_str_has_prefix(img->exif_lens, "TAMRON ") || g_str_has_prefix(img->exif_lens, "VILTROX")) { for(size_t i = 1; i <= 6; ++i) img->exif_lens[i] = g_ascii_tolower(img->exif_lens[i]); } // Handle Sigma lenses on modern bodies (e.g. Nikon Z) using already clean // lensMake and lensModel. if(Imf::hasLensMake(header) && Imf::lensMake(header) == "SIGMA" && g_ascii_strcasecmp(img->exif_lens, "Sigma")) { std::string pretty = "Sigma " + std::string(img->exif_lens); g_strlcpy(img->exif_lens, pretty.c_str(), sizeof(img->exif_lens)); } } if(Imf::hasNominalFocalLength(header)) img->exif_focal_length = Imf::nominalFocalLength(header); #endif } } /* get image width and height from data window only */ dw = header.dataWindow(); img->width = dw.max.x - dw.min.x + 1; img->height = dw.max.y - dw.min.y + 1; // Try to allocate image data img->buf_dsc.channels = 4; img->buf_dsc.datatype = TYPE_FLOAT; float *buf = (float *)dt_mipmap_cache_alloc(mbuf, img); if(!buf) { dt_print(DT_DEBUG_ALWAYS, "[exr_open] error: could not alloc full buffer for image '%s'", img->filename); return DT_IMAGEIO_CACHE_FULL; } /* set up frame buffer relative to data window */ dx = dw.min.x; dy = dw.min.y; xstride = sizeof(float) * 4; ystride = sizeof(float) * img->width * 4; frameBuffer.insert( "R", Imf::Slice(Imf::FLOAT, (char *)(buf - dx * 4 - dy * img->width * 4 + 0), xstride, ystride, 1, 1, 0.0)); frameBuffer.insert( "G", Imf::Slice(Imf::FLOAT, (char *)(buf - dx * 4 - dy * img->width * 4 + 1), xstride, ystride, 1, 1, 0.0)); frameBuffer.insert( "B", Imf::Slice(Imf::FLOAT, (char *)(buf - dx * 4 - dy * img->width * 4 + 2), xstride, ystride, 1, 1, 0.0)); frameBuffer.insert( "A", Imf::Slice(Imf::FLOAT, (char *)(buf - dx * 4 - dy * img->width * 4 + 3), xstride, ystride, 1, 1, 0.0)); try { if(isTiled) { fileTiled->setFrameBuffer(frameBuffer); fileTiled->readTiles(0, fileTiled->numXTiles() - 1, 0, fileTiled->numYTiles() - 1); } else { file->setFrameBuffer(frameBuffer); file->readPixels(dw.min.y, dw.max.y); } } catch(const std::exception &e) { dt_print(DT_DEBUG_ALWAYS, "[exr_open] error: could not read from '%s' due to: %s", img->filename, e.what()); return DT_IMAGEIO_FILE_CORRUPTED; } /* try to get the chromaticities and whitepoint. this will add the * default linear rec709 profile when nothing was embedded and look * as if it was embedded in colorin. better than defaulting to * something wrong there. */ Imf::Chromaticities chromaticities; float whiteLuminance = 1.0f; if(Imf::hasChromaticities(header)) { chromaticities = Imf::chromaticities(header); /* adapt chromaticities to D65 expected by colorin */ cmsCIExyY red_xy = { chromaticities.red[0], chromaticities.red[1], 1.0 }; cmsCIEXYZ srcRed; cmsxyY2XYZ(&srcRed, &red_xy); cmsCIExyY green_xy = { chromaticities.green[0], chromaticities.green[1], 1.0 }; cmsCIEXYZ srcGreen; cmsxyY2XYZ(&srcGreen, &green_xy); cmsCIExyY blue_xy = { chromaticities.blue[0], chromaticities.blue[1], 1.0 }; cmsCIEXYZ srcBlue; cmsxyY2XYZ(&srcBlue, &blue_xy); const cmsCIExyY srcWhite_xy = { chromaticities.white[0], chromaticities.white[1], 1.0 }; cmsCIEXYZ srcWhite; cmsxyY2XYZ(&srcWhite, &srcWhite_xy); /* use Imf::Chromaticities definition */ const cmsCIExyY d65_xy = {0.3127f, 0.3290f, 1.0}; cmsCIEXYZ d65; cmsxyY2XYZ(&d65, &d65_xy); cmsCIEXYZ dstRed; cmsAdaptToIlluminant(&dstRed, &srcWhite, &d65, &srcRed); cmsCIEXYZ dstGreen; cmsAdaptToIlluminant(&dstGreen, &srcWhite, &d65, &srcGreen); cmsCIEXYZ dstBlue; cmsAdaptToIlluminant(&dstBlue, &srcWhite, &d65, &srcBlue); cmsXYZ2xyY(&red_xy, &dstRed); chromaticities.red[0] = (float)red_xy.x; chromaticities.red[1] = (float)red_xy.y; cmsXYZ2xyY(&green_xy, &dstGreen); chromaticities.green[0] = (float)green_xy.x; chromaticities.green[1] = (float)green_xy.y; cmsXYZ2xyY(&blue_xy, &dstBlue); chromaticities.blue[0] = (float)blue_xy.x; chromaticities.blue[1] = (float)blue_xy.y; chromaticities.white[0] = 0.3127f; chromaticities.white[1] = 0.3290f; } if(Imf::hasWhiteLuminance(header)) whiteLuminance = Imf::whiteLuminance(header); Imath::M44f m = Imf::XYZtoRGB(chromaticities, whiteLuminance); for(int i = 0; i < 3; i++) for(int j = 0; j < 3; j++) { img->d65_color_matrix[3 * i + j] = m[j][i]; } img->buf_dsc.cst = IOP_CS_RGB; img->buf_dsc.filters = 0u; img->flags &= ~DT_IMAGE_RAW; img->flags &= ~DT_IMAGE_S_RAW; img->flags &= ~DT_IMAGE_LDR; img->flags |= DT_IMAGE_HDR; img->loader = LOADER_EXR; return DT_IMAGEIO_OK; } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on