/* This file is part of darktable, Copyright (C) 2010-2025 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 . */ #ifdef _OPENMP #include #endif #include "RawSpeed-API.h" #include "io/FileIOException.h" #include "metadata/CameraMetadataException.h" #include "parsers/RawParserException.h" #include "parsers/FiffParserException.h" #define TYPE_FLOAT32 RawImageType::F32 #define TYPE_USHORT16 RawImageType::UINT16 #include #include #define __STDC_LIMIT_MACROS #include "common/colorspaces.h" #include "common/darktable.h" #include "common/exif.h" #include "common/file_location.h" #include "common/tags.h" #include "develop/imageop.h" #include "imageio/imageio_common.h" #include "imageio/imageio_rawspeed.h" #include // define this function, it is only declared in rawspeed: int rawspeed_get_number_of_processor_cores() { #ifdef _OPENMP return omp_get_num_procs(); #else return 1; #endif } using namespace rawspeed; namespace { // rawspeed throws, and an unlock after the call is skipped when it does // common/exif.cc does the same for exiv2, with its Lock class class SCOPED_CAPABILITY _mutex_guard { public: explicit _mutex_guard(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) : _mutex(mutex) { dt_pthread_mutex_lock(mutex); } ~_mutex_guard() RELEASE() { dt_pthread_mutex_unlock(_mutex); } _mutex_guard(const _mutex_guard &) = delete; _mutex_guard &operator=(const _mutex_guard &) = delete; private: dt_pthread_mutex_t *_mutex; }; } // namespace static dt_imageio_retval_t dt_imageio_open_rawspeed_sraw (dt_image_t *img, const RawImage r, dt_mipmap_buffer_t *buf); static CameraMetaData *meta = NULL; static std::once_flag meta_once; static void dt_rawspeed_load_meta() { /* Load rawspeed cameras.xml meta file once */ // not under plugin_threadsafe: import gets here holding the image's cache // write lock, and disk export takes image cache locks while holding // plugin_threadsafe (storage/disk.c) std::call_once(meta_once, [] { char datadir[PATH_MAX] = { 0 }, camfile[PATH_MAX] = { 0 }; dt_loc_get_datadir(datadir, sizeof(datadir)); snprintf(camfile, sizeof(camfile), "%s/rawspeed/cameras.xml", datadir); // never cleaned up (only when dt closes) meta = new CameraMetaData(camfile); }); } gboolean dt_rawspeed_lookup_makermodel(const char *maker, const char *model, char *mk, const int mk_len, char *md, const int md_len, char *al, const int al_len) { gboolean got_it_done = FALSE; try { dt_rawspeed_load_meta(); // Look for camera in any mode available const Camera *cam = meta->getCamera(maker, model); if(cam) { g_strlcpy(mk, cam->canonical_make.c_str(), mk_len); g_strlcpy(md, cam->canonical_model.c_str(), md_len); g_strlcpy(al, cam->canonical_alias.c_str(), al_len); got_it_done = TRUE; } } catch(const std::exception &exc) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] %s", exc.what()); } if(!got_it_done) { // We couldn't find the camera or caught some exception, just punt and pass // through the same values g_strlcpy(mk, maker, mk_len); g_strlcpy(md, model, md_len); g_strlcpy(al, model, al_len); } return got_it_done; } uint32_t dt_rawspeed_crop_dcraw_filters(const uint32_t filters, const uint32_t crop_x, const uint32_t crop_y) { if(!filters || filters == 9u) return filters; else return ColorFilterArray::shiftDcrawFilter(filters, crop_x, crop_y); } // CR3 files are for now handled by LibRaw, we do not want RawSpeed to try to open them // as this issues a lot of error messages on the console. static gboolean _ignore_image(const gchar *filename) { gchar *extensions_ignorelist; const gchar *always_ignore = "cr3"; gchar *ext = g_strrstr(filename, "."); if(!ext) return FALSE; ext++; if(dt_conf_key_not_empty("libraw_extensions")) extensions_ignorelist = g_strjoin(" ", always_ignore, dt_conf_get_string_const("libraw_extensions"), (char *)NULL); else extensions_ignorelist = g_strdup(always_ignore); dt_print(DT_DEBUG_IMAGEIO, "[rawspeed_open] extensions list to ignore: `%s'", extensions_ignorelist); gchar *ext_lowercased = g_ascii_strdown(ext,-1); if(g_strstr_len(extensions_ignorelist,-1,ext_lowercased)) { g_free(extensions_ignorelist); g_free(ext_lowercased); return TRUE; } g_free(extensions_ignorelist); g_free(ext_lowercased); return FALSE; } dt_imageio_retval_t dt_imageio_open_rawspeed(dt_image_t *img, const char *filename, dt_mipmap_buffer_t *mbuf) { if(_ignore_image(filename)) return DT_IMAGEIO_UNSUPPORTED_FORMAT; if(!img) { dt_print(DT_DEBUG_ALWAYS, "[dt_imageio_open_rawspeed] failed to get dt_image_t for '%s' at %p", filename, mbuf); return DT_IMAGEIO_LOAD_FAILED; } if(!img->exif_inited) (void)dt_exif_read(img, filename); char filen[PATH_MAX] = { 0 }; snprintf(filen, sizeof(filen), "%s", filename); FileReader f(filen); try { dt_rawspeed_load_meta(); // scoped to the read alone: holding the guard for the whole try block // would serialize decoding too auto [storage, storageBuf] = [&f]() { const _mutex_guard lock(&darktable.readFile_mutex); return f.readFile(); }(); RawParser t(storageBuf); std::unique_ptr d = t.getDecoder(meta); if(!d.get()) return DT_IMAGEIO_UNSUPPORTED_FORMAT; d->failOnUnknown = true; d->checkSupport(meta); d->decodeRaw(); d->decodeMetaData(meta); RawImage r = d->mRaw; const auto errors = r->getErrors(); for(const auto &error : errors) dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) %s", img->filename, error.c_str()); g_strlcpy(img->camera_maker, r->metadata.canonical_make.c_str(), sizeof(img->camera_maker)); g_strlcpy(img->camera_model, r->metadata.canonical_model.c_str(), sizeof(img->camera_model)); g_strlcpy(img->camera_alias, r->metadata.canonical_alias.c_str(), sizeof(img->camera_alias)); dt_image_refresh_makermodel(img); img->raw_black_level = r->blackLevel; img->raw_white_point = r->whitePoint.value_or((1U << 16)-1); // NOTE: while it makes sense to always sample black areas when they exist, // black area handling is broken in rawspeed, so don't do that for now. // https://github.com/darktable-org/rawspeed/issues/389 if(!r->blackLevelSeparate) { r->calculateBlackAreas(); } const auto bl = *(r->blackLevelSeparate->getAsArray1DRef()); for(uint8_t i = 0; i < 4; i++) img->raw_black_level_separate[i] = bl(i); if(r->blackLevel == -1) { float black = 0.0f; for(uint8_t i = 0; i < 4; i++) { black += img->raw_black_level_separate[i]; } black /= 4.0f; img->raw_black_level = CLAMP(roundf(black), 0, UINT16_MAX); } /* * FIXME * if(r->whitePoint == 65536) * ??? */ /* free auto pointers on spot */ d.reset(); storage.reset(); // Grab the WB if(r->metadata.wbCoeffs) { for(int i = 0; i < 4; i++) img->wb_coeffs[i] = (*r->metadata.wbCoeffs)[i]; } else { for(int i = 0; i < 4; i++) img->wb_coeffs[i] = 0.0; } // Grab the Adobe coeffs const int msize = r->metadata.colorMatrix.size(); for(int k = 0; k < 4; k++) for(int i = 0; i < 3; i++) { const int idx = k*3 + i; if(idx < msize) img->adobe_XYZ_to_CAM[k][i] = float(r->metadata.colorMatrix[idx]); else img->adobe_XYZ_to_CAM[k][i] = 0.0f; } // Get additional exif tags that are not cached in the database dt_exif_img_check_additional_tags(img, filename); if(r->getDataType() == TYPE_FLOAT32) { img->flags |= DT_IMAGE_HDR; // We assume that float images should already be normalized. // Also consider 1.0f in binary32 (legacy dt HDR files) as white point magic value; // otherwise (e.g. HDRMerge files), let rawprepare normalize as usual. if(r->whitePoint == 0x3F800000) img->raw_white_point = 1; if(img->raw_white_point == 1) for(int k = 0; k < 4; k++) img->buf_dsc.processed_maximum[k] = 1.0f; } img->buf_dsc.filters = 0u; // dimensions of uncropped image const iPoint2D dimUncropped = r->getUncroppedDim(); img->width = dimUncropped.x; img->height = dimUncropped.y; // dimensions of cropped image const iPoint2D dimCropped = r->dim; // crop - Top,Left corner const iPoint2D cropTL = r->getCropOffset(); img->crop_x = cropTL.x; img->crop_y = cropTL.y; // crop - Bottom,Right corner const iPoint2D cropBR = dimUncropped - dimCropped - cropTL; img->crop_right = cropBR.x; img->crop_bottom = cropBR.y; img->p_width = img->width - img->crop_x - img->crop_right; img->p_height = img->height - img->crop_y - img->crop_bottom; img->fuji_rotation_pos = r->metadata.fujiRotationPos; img->pixel_aspect_ratio = (float)r->metadata.pixelAspectRatio; if(!r->isCFA) { const dt_imageio_retval_t ret = dt_imageio_open_rawspeed_sraw(img, r, mbuf); return ret; } if((r->getDataType() != TYPE_USHORT16) && (r->getDataType() != TYPE_FLOAT32)) return DT_IMAGEIO_UNSUPPORTED_FEATURE; if((r->getBpp() != sizeof(uint16_t)) && (r->getBpp() != sizeof(float))) return DT_IMAGEIO_UNSUPPORTED_FEATURE; if((r->getDataType() == TYPE_USHORT16) && (r->getBpp() != sizeof(uint16_t))) return DT_IMAGEIO_UNSUPPORTED_FEATURE; if((r->getDataType() == TYPE_FLOAT32) && (r->getBpp() != sizeof(float))) return DT_IMAGEIO_UNSUPPORTED_FEATURE; const float cpp = r->getCpp(); if(cpp != 1) return DT_IMAGEIO_LOAD_FAILED; img->buf_dsc.channels = 1; switch(r->getBpp()) { case sizeof(uint16_t): img->buf_dsc.datatype = TYPE_UINT16; break; case sizeof(float): img->buf_dsc.datatype = TYPE_FLOAT; break; default: return DT_IMAGEIO_UNSUPPORTED_FEATURE; } // as the X-Trans filters comments later on states, these are for // cropped image, so we need to uncrop them. img->buf_dsc.filters = dt_rawspeed_crop_dcraw_filters(r->cfa.getDcrawFilter(), cropTL.x, cropTL.y); if(FILTERS_ARE_4BAYER(img->buf_dsc.filters)) img->flags |= DT_IMAGE_4BAYER; if(img->buf_dsc.filters) { img->flags &= ~DT_IMAGE_LDR; img->flags |= DT_IMAGE_RAW; // special handling for x-trans sensors if(img->buf_dsc.filters == 9u) { // get 6x6 CFA offset from top left of cropped image // NOTE: This is different from how things are done with Bayer // sensors. For these, the CFA in cameras.xml is pre-offset // depending on the distance modulo 2 between raw and usable // image data. For X-Trans, the CFA in cameras.xml is // (currently) aligned with the top left of the raw data. for(int i = 0; i < 6; ++i) for(int j = 0; j < 6; ++j) { img->buf_dsc.xtrans[j][i] = (uint8_t)r->cfa.getColorAt(i % 6, j % 6); } } } // if buf is NULL, we quit the fct here if(!mbuf) { img->buf_dsc.cst = IOP_CS_RAW; img->loader = LOADER_RAWSPEED; return DT_IMAGEIO_OK; } void *buf = dt_mipmap_cache_alloc(mbuf, img); if(!buf) return DT_IMAGEIO_CACHE_FULL; /* * since we do not want to crop black borders at this stage, * and we do not want to rotate image, we can just use memcpy, * as it is faster than dt_imageio_flip_buffers, but only if * buffer sizes are equal, * (from Klaus: r->pitch may differ from DT pitch (line to line spacing)) * else fallback to generic dt_imageio_flip_buffers() */ const size_t bufSize_mipmap = (size_t)img->width * img->height * r->getBpp(); const size_t bufSize_rawspeed = (size_t)r->pitch * dimUncropped.y; if(bufSize_mipmap == bufSize_rawspeed) { memcpy(buf, (char *)(&(r->getByteDataAsUncroppedArray2DRef()(0, 0))), bufSize_mipmap); } else { dt_imageio_flip_buffers((char *)buf, (char *)(&(r->getByteDataAsUncroppedArray2DRef()(0, 0))), r->getBpp(), dimUncropped.x, dimUncropped.y, dimUncropped.x, dimUncropped.y, r->pitch, ORIENTATION_NONE); } // Check if the camera is missing samples const Camera *cam = meta->getCamera(r->metadata.make.c_str(), r->metadata.model.c_str(), r->metadata.mode.c_str()); if(cam && cam->supportStatus == Camera::SupportStatus::SupportedNoSamples) img->camera_missing_sample = TRUE; } catch(const rawspeed::IOException &exc) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) I/O error: %s", img->filename, exc.what()); return DT_IMAGEIO_IOERROR; } catch(const rawspeed::FileIOException &exc) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) File I/O error: %s", img->filename, exc.what()); return DT_IMAGEIO_IOERROR; } catch(const rawspeed::RawDecoderException &exc) { const char *msg = exc.what(); // FIXME FIXME // The following is a nasty hack which will break if exception messages change. // The proper way to handle this is to add two new exception types to Rawspeed and // have them throw the appropriate ones on encountering an unsupported camera model // or unsupported feature (e.g. bit depth, compression, aspect ratio mode, ...) if(msg && (strstr(msg, "Camera not supported") || strstr(msg, "not supported, and not allowed to guess"))) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] Unsupported camera model for %s", img->filename); return DT_IMAGEIO_UNSUPPORTED_CAMERA; } else if (msg && strstr(msg, "supported")) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) %s", img->filename, msg); return DT_IMAGEIO_UNSUPPORTED_FEATURE; } else { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] %s corrupt: %s", img->filename, exc.what()); // We can end up here if the loader is called on what is actually // a TIFF file, which mistakenly contains the signature of a raw // format in a TIFF container. So it is very important that the // return code from here directs the execution path through the // fallback loader chain. return DT_IMAGEIO_UNSUPPORTED_FORMAT; } } catch(const rawspeed::RawParserException &exc) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) CIFF/FIFF error: %s", img->filename, exc.what()); return DT_IMAGEIO_UNSUPPORTED_FORMAT; } catch(const rawspeed::CameraMetadataException &exc) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) metadata error: %s", img->filename, exc.what()); return DT_IMAGEIO_UNSUPPORTED_FEATURE; } catch(const std::exception &exc) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] (%s) %s", img->filename, exc.what()); /* if an exception is raised lets not retry or handle the specific ones, consider the file as corrupted */ return DT_IMAGEIO_FILE_CORRUPTED; } catch(...) { dt_print(DT_DEBUG_ALWAYS, "[rawspeed] unhandled exception in imageio_rawspeed"); return DT_IMAGEIO_FILE_CORRUPTED; } img->buf_dsc.cst = IOP_CS_RAW; img->loader = LOADER_RAWSPEED; return DT_IMAGEIO_OK; } dt_imageio_retval_t dt_imageio_open_rawspeed_sraw(dt_image_t *img, const RawImage r, dt_mipmap_buffer_t *mbuf) { // sraw aren't real raw, but not ldr either (need white balance and stuff) img->flags &= ~DT_IMAGE_LDR; img->flags &= ~DT_IMAGE_RAW; img->flags |= DT_IMAGE_S_RAW; // actually we want to store full floats here: img->buf_dsc.channels = 4; img->buf_dsc.datatype = TYPE_FLOAT; if(r->getDataType() != TYPE_USHORT16 && r->getDataType() != TYPE_FLOAT32) return DT_IMAGEIO_UNSUPPORTED_FEATURE; const uint32_t cpp = r->getCpp(); if(cpp != 1 && cpp != 3 && cpp != 4) return DT_IMAGEIO_FILE_CORRUPTED; // if buf is NULL, we quit the fct here if(!mbuf) { img->buf_dsc.cst = IOP_CS_RAW; img->loader = LOADER_RAWSPEED; return DT_IMAGEIO_OK; } if(cpp == 1) img->flags |= DT_IMAGE_MONOCHROME; void *buf = dt_mipmap_cache_alloc(mbuf, img); if(!buf) return DT_IMAGEIO_CACHE_FULL; if(cpp == 1) { /* * monochrome image (e.g. Leica M9 monochrom), * we need to copy data from only channel to each of 3 channels */ if(r->getDataType() == TYPE_USHORT16) { DT_OMP_PRAGMA(parallel for num_threads(darktable.num_openmp_threads) schedule(static) shared(r, img, buf) firstprivate(cpp)) for(int j = 0; j < img->height; j++) { const Array2DRef in = r->getU16DataAsUncroppedArray2DRef(); float *out = ((float *)buf) + (size_t)4 * j * img->width; for(int i = 0; i < img->width; i++, out += 4) { out[0] = out[1] = out[2] = (float)in(j, cpp * i) / (float)UINT16_MAX; out[3] = 0.0f; } } } else // r->getDataType() == TYPE_FLOAT32 { DT_OMP_PRAGMA(parallel for num_threads(darktable.num_openmp_threads) schedule(static) shared(r, img, buf) firstprivate(cpp)) for(int j = 0; j < img->height; j++) { const Array2DRef in = r->getF32DataAsUncroppedArray2DRef(); float *out = ((float *)buf) + (size_t)4 * j * img->width; for(int i = 0; i < img->width; i++, out += 4) { out[0] = out[1] = out[2] = in(j, cpp * i); out[3] = 0.0f; } } } } else // case cpp == 3 or 4 { /* * standard 3-ch image * just copy 3 ch to 3 ch */ if(r->getDataType() == TYPE_USHORT16) { DT_OMP_PRAGMA(parallel for num_threads(darktable.num_openmp_threads) schedule(static) shared(r, img, buf) firstprivate(cpp)) for(int j = 0; j < img->height; j++) { const Array2DRef in = r->getU16DataAsUncroppedArray2DRef(); float *out = ((float *)buf) + (size_t)4 * j * img->width; for(int i = 0; i < img->width; i++, out += 4) { for(int k = 0; k < 3; k++) out[k] = (float)in(j, cpp * i + k) / (float)UINT16_MAX; out[3] = 0.0f; } } } else // r->getDataType() == TYPE_FLOAT32 { DT_OMP_PRAGMA(parallel for num_threads(darktable.num_openmp_threads) schedule(static) shared(r, img, buf) firstprivate(cpp)) for(int j = 0; j < img->height; j++) { const Array2DRef in = r->getF32DataAsUncroppedArray2DRef(); float *out = ((float *)buf) + (size_t)4 * j * img->width; for(int i = 0; i < img->width; i++, out += 4) { for(int k = 0; k < 3; k++) out[k] = in(j, cpp * i + k); out[3] = 0.0f; } } } } img->buf_dsc.cst = IOP_CS_RGB; img->loader = LOADER_RAWSPEED; // Check if the camera is missing samples const Camera *cam = meta->getCamera(r->metadata.make.c_str(), r->metadata.model.c_str(), r->metadata.mode.c_str()); if(cam && cam->supportStatus == Camera::SupportStatus::SupportedNoSamples) img->camera_missing_sample = TRUE; 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