diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in
index 8e106a6e1557..ea927ef550f9 100644
--- a/data/darktableconfig.xml.in
+++ b/data/darktableconfig.xml.in
@@ -273,6 +273,13 @@
color manage cached thumbnails
if enabled, cached thumbnails will be color managed so that lighttable and filmstrip can show correct colors. otherwise the results may look wrong once the display profile gets changed.
+
+ cache/import_raw_jpeg_optimization
+ bool
+ true
+ optimize RAW+JPEG import using JPEG for thumbnails
+ when enabled and both RAW and JPEG files are imported together, the JPEG file will be used to quickly populate the mipmap cache instead of expensive RAW demosaicing. this significantly speeds up thumbnail generation. the feature is only active when companion JPEG files are found during RAW import.
+
opencl_device_priority
string
diff --git a/src/cli/main.c b/src/cli/main.c
index e7a747c56bbf..34686d7fac57 100644
--- a/src/cli/main.c
+++ b/src/cli/main.c
@@ -569,7 +569,8 @@ int main(int argc, char *arg[])
if(!g_file_test(fullname, G_FILE_TEST_IS_DIR) && dt_supported_image(fname))
{
// Import each supported image file directly
- const dt_imgid_t imgid = dt_image_import(filmid, fullname, TRUE, TRUE);
+ const dt_imgid_t imgid =
+ dt_image_import(filmid, fullname, NULL, TRUE, TRUE);
if(dt_is_valid_imgid(imgid))
{
id_list = g_list_append(id_list, GINT_TO_POINTER(imgid));
@@ -598,7 +599,7 @@ int main(int argc, char *arg[])
gchar *directory = g_path_get_dirname(input);
filmid = dt_film_new(&film, directory);
- const dt_imgid_t id = dt_image_import(filmid, input, TRUE, TRUE);
+ const dt_imgid_t id = dt_image_import(filmid, input, NULL, TRUE, TRUE);
g_free(directory);
if(!dt_is_valid_imgid(id))
{
diff --git a/src/common/camera_control.c b/src/common/camera_control.c
index fddf86ef362e..9d17b1feee2a 100644
--- a/src/common/camera_control.c
+++ b/src/common/camera_control.c
@@ -1213,9 +1213,47 @@ static gboolean _camera_initialize(const dt_camctl_t *c,
return TRUE;
}
+static int _ext_priority(const char *ext) {
+ /* Ensure JPEG/preview is imported before RAW for identical basenames
+ — this populates the mipmap/preview cache (speeding thumbnails) */
+ if (dt_imageio_is_raw_preview_by_extension(ext)) {
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
static int _sort_filename(gchar *a, gchar *b)
{
- return g_strcmp0(a, b);
+ /* compare by basename (without extension); if equal prefer extensions by
+ * priority */
+ gchar *ba = g_path_get_basename(a);
+ gchar *bb = g_path_get_basename(b);
+
+ char *da = strrchr(ba, '.');
+ char *db = strrchr(bb, '.');
+
+ if (da)
+ *da = '\0';
+ if (db)
+ *db = '\0';
+
+ int res = g_strcmp0(ba, bb);
+ if (res == 0) {
+ /* same basename, decide by extension priority */
+ const char *exta = da ? da + 1 : NULL;
+ const char *extb = db ? db + 1 : NULL;
+ int pa = _ext_priority(exta);
+ int pb = _ext_priority(extb);
+ if (pa != pb)
+ res = pa - pb;
+ else
+ res = g_strcmp0(a, b);
+ }
+
+ g_free(ba);
+ g_free(bb);
+ return res;
}
void dt_camctl_import(const dt_camctl_t *c,
diff --git a/src/common/darktable.c b/src/common/darktable.c
index 9a48b38bc72d..9bc34f62a93b 100644
--- a/src/common/darktable.c
+++ b/src/common/darktable.c
@@ -447,7 +447,7 @@ dt_imgid_t dt_load_from_string(const gchar *input,
gchar *directory = g_path_get_dirname((const gchar *)filename);
dt_film_t film;
const dt_filmid_t filmid = dt_film_new(&film, directory);
- imgid = dt_image_import(filmid, filename, TRUE, TRUE);
+ imgid = dt_image_import(filmid, filename, NULL, TRUE, TRUE);
g_free(directory);
if(dt_is_valid_imgid(imgid))
{
diff --git a/src/common/image.c b/src/common/image.c
index c36b92b93939..bd85d16e61f7 100644
--- a/src/common/image.c
+++ b/src/common/image.c
@@ -1795,10 +1795,10 @@ static int _image_read_duplicates(const uint32_t id,
static dt_imgid_t _image_import_internal(const dt_filmid_t film_id,
const char *filename,
+ const char *preview_jpeg_filepath,
const gboolean override_ignore_nonraws,
const gboolean lua_locking,
- const gboolean raise_signals)
-{
+ const gboolean raise_signals) {
char *normalized_filename = dt_util_normalize_path(filename);
if(!normalized_filename || !dt_util_test_image_file(normalized_filename))
{
@@ -2056,6 +2056,16 @@ static dt_imgid_t _image_import_internal(const dt_filmid_t film_id,
// make sure that there are no stale thumbnails left
dt_mipmap_cache_remove(id);
+ // Try to optimize RAW+JPEG import by using companion JPEG for mipmap cache
+ // This speeds up thumbnail generation by avoiding RAW demosaicing
+ if (preview_jpeg_filepath != NULL) {
+ dt_print(DT_DEBUG_ALWAYS,
+ "[image_import_internal] importing companion JPEG `%s' for mipmap "
+ "cache of img id %d\n",
+ preview_jpeg_filepath, id);
+ dt_mipmap_cache_import_jpeg_to_mips(id, preview_jpeg_filepath);
+ }
+
// Always keep write timestamp in database and possibly write xmp
dt_image_synch_all_xmp(normalized_filename);
@@ -2137,20 +2147,19 @@ dt_imgid_t dt_image_get_id(const dt_filmid_t film_id,
return id;
}
-dt_imgid_t dt_image_import(const dt_filmid_t film_id,
- const char *filename,
+dt_imgid_t dt_image_import(const dt_filmid_t film_id, const char *filename,
+ const char *preview_jpeg_filepath,
const gboolean override_ignore_nonraws,
- const gboolean raise_signals)
-{
- return _image_import_internal(film_id, filename, override_ignore_nonraws,
- TRUE, raise_signals);
+ const gboolean raise_signals) {
+ return _image_import_internal(film_id, filename, preview_jpeg_filepath,
+ override_ignore_nonraws, TRUE, raise_signals);
}
-dt_imgid_t dt_image_import_lua(const dt_filmid_t film_id,
- const char *filename,
- const gboolean override_ignore_nonraws)
-{
- return _image_import_internal(film_id, filename, override_ignore_nonraws, FALSE, TRUE);
+dt_imgid_t dt_image_import_lua(const dt_filmid_t film_id, const char *filename,
+ const char *preview_jpeg_filepath,
+ const gboolean override_ignore_nonraws) {
+ return _image_import_internal(film_id, filename, preview_jpeg_filepath,
+ override_ignore_nonraws, FALSE, TRUE);
}
void dt_image_init(dt_image_t *img)
diff --git a/src/common/image.h b/src/common/image.h
index 0ab41c21597e..7386e5f557da 100644
--- a/src/common/image.h
+++ b/src/common/image.h
@@ -444,14 +444,14 @@ dt_imgid_t dt_image_get_id_full_path(const gchar *filename);
dt_imgid_t dt_image_get_id(const dt_filmid_t film_id,
const gchar *filename);
/** imports a new image from raw/etc file and adds it to the data base and image cache. Use from threads other than lua.*/
-dt_imgid_t dt_image_import(dt_filmid_t film_id,
- const char *filename,
+dt_imgid_t dt_image_import(dt_filmid_t film_id, const char *filename,
+ const char *preview_jpeg_filepath,
const gboolean override_ignore_nonraws,
const gboolean raise_signals);
/** imports a new image from raw/etc file and adds it to the data base
* and image cache. Use from lua thread.*/
-dt_imgid_t dt_image_import_lua(const dt_filmid_t film_id,
- const char *filename,
+dt_imgid_t dt_image_import_lua(const dt_filmid_t film_id, const char *filename,
+ const char *preview_jpeg_filepath,
const gboolean override_ignore_nonraws);
/** removes the given image from the database. */
void dt_image_remove(const dt_imgid_t imgid);
diff --git a/src/common/import_session.c b/src/common/import_session.c
index 03d4212c5b6a..e606b0d7be92 100644
--- a/src/common/import_session.c
+++ b/src/common/import_session.c
@@ -187,7 +187,8 @@ void dt_import_session_unref(dt_import_session_t *self)
void dt_import_session_import(dt_import_session_t *self)
{
- const dt_imgid_t imgid = dt_image_import(self->film->id, self->current_filename, TRUE, TRUE);
+ const dt_imgid_t imgid =
+ dt_image_import(self->film->id, self->current_filename, NULL, TRUE, TRUE);
if(dt_is_valid_imgid(imgid))
{
DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, imgid);
diff --git a/src/common/mipmap_cache.c b/src/common/mipmap_cache.c
index 8676d4e1d750..336203e879b0 100644
--- a/src/common/mipmap_cache.c
+++ b/src/common/mipmap_cache.c
@@ -1715,6 +1715,109 @@ void dt_mipmap_cache_copy_thumbnails(const dt_imgid_t dst_imgid,
}
}
+// Optimize RAW+JPEG import by using companion JPEG file to populate mipmap
+// cache. When RAW and JPEG files are imported together, this function reads the
+// JPEG and uses it to populate mipmap level 8, then downscales to generate
+// levels 0-7. This avoids expensive RAW demosaicing for thumbnail generation.
+// Returns TRUE if optimization was applied, FALSE if no companion JPEG found or
+// feature disabled.
+gboolean dt_mipmap_cache_import_jpeg_to_mips(const dt_imgid_t imgid,
+ const char *filename) {
+ // Check if feature is enabled via preference
+ if (!dt_conf_get_bool("cache/import_raw_jpeg_optimization"))
+ return FALSE;
+
+ if (!dt_is_valid_imgid(imgid) || !filename || !filename[0])
+ return FALSE;
+
+ // Read the JPEG file into memory
+ uint8_t *jpeg_blob = NULL;
+ size_t jpeg_blob_len = 0;
+ GError *gerror = NULL;
+
+ if (!g_file_get_contents(filename, (gchar **)&jpeg_blob, &jpeg_blob_len,
+ &gerror)) {
+ dt_print(DT_DEBUG_CACHE,
+ "[mipmap_cache] failed to read companion JPEG %s: %s", filename,
+ gerror->message);
+ g_error_free(gerror);
+ return FALSE;
+ }
+
+ // Decompress JPEG header to get dimensions and color space
+ dt_imageio_jpeg_t jpg;
+ if (dt_imageio_jpeg_decompress_header(jpeg_blob, jpeg_blob_len, &jpg)) {
+ dt_print(DT_DEBUG_CACHE,
+ "[mipmap_cache] failed to read JPEG header from %s", filename);
+ g_free(jpeg_blob);
+ return FALSE;
+ }
+
+ // Get write-locked mipmap buffer for level 8 (full preview)
+ dt_mipmap_buffer_t buf;
+ dt_mipmap_cache_get(&buf, imgid, DT_MIPMAP_8, DT_MIPMAP_BLOCKING, 'w');
+
+ if (!buf.cache_entry || !buf.buf) {
+ dt_print(DT_DEBUG_CACHE,
+ "[mipmap_cache] failed to get mipmap 8 buffer for image %u",
+ imgid);
+ g_free(jpeg_blob);
+ return FALSE;
+ }
+
+ // Decompress JPEG into a temporary buffer first
+ uint8_t *tmp = dt_alloc_align_uint8((size_t)jpg.width * jpg.height * 4);
+ if (!tmp) {
+ dt_print(DT_DEBUG_CACHE,
+ "[mipmap_cache] memory allocation failed for temp jpeg buffer for "
+ "image %u",
+ imgid);
+ dt_mipmap_cache_release(&buf);
+ g_free(jpeg_blob);
+ return FALSE;
+ }
+
+ if (dt_imageio_jpeg_decompress(&jpg, tmp)) {
+ dt_print(DT_DEBUG_CACHE,
+ "[mipmap_cache] failed to decompress JPEG for image %u", imgid);
+ dt_free_align(tmp);
+ dt_mipmap_cache_release(&buf);
+ g_free(jpeg_blob);
+ return FALSE;
+ }
+
+ // Apply RAW image orientation so the cached image matches the RAW
+ const dt_image_orientation_t orientation = dt_image_get_orientation(imgid);
+
+ // Destination buffer is the cache payload
+ dt_mipmap_buffer_dsc_t *dsc = (dt_mipmap_buffer_dsc_t *)buf.cache_entry->data;
+ uint32_t out_w = 0, out_h = 0;
+ // use flip_and_zoom with destination size equal to source, so we just
+ // reorient without scaling
+ dt_iop_flip_and_zoom_8(tmp, jpg.width, jpg.height, buf.buf, jpg.width,
+ jpg.height, orientation, &out_w, &out_h);
+
+ dsc->width = out_w;
+ dsc->height = out_h;
+ dsc->iscale = 1.0f;
+ dsc->color_space = dt_imageio_jpeg_read_color_space(&jpg);
+ dsc->flags &= ~DT_MIPMAP_BUFFER_DSC_FLAG_GENERATE; // Mark as generated
+
+ dt_print(DT_DEBUG_CACHE,
+ "[mipmap_cache] populated mipmap 8 (%dx%d) from companion JPEG for "
+ "image %u (oriented)",
+ dsc->width, dsc->height, imgid);
+
+ dt_free_align(tmp);
+ dt_mipmap_cache_release(&buf);
+ g_free(jpeg_blob);
+
+ // TODO: Generate mipmap levels 0-7 by downscaling from level 8
+ // This would require cairo or similar scaling infrastructure
+
+ return TRUE;
+}
+
// 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
diff --git a/src/common/mipmap_cache.h b/src/common/mipmap_cache.h
index 1c3aef8e3c63..8b8e51bc7052 100644
--- a/src/common/mipmap_cache.h
+++ b/src/common/mipmap_cache.h
@@ -156,6 +156,13 @@ void dt_mipmap_cache_copy_thumbnails(const dt_imgid_t dst_imgid, const dt_imgid_
// return the mipmap corresponding to text value saved in prefs
dt_mipmap_size_t dt_mipmap_cache_get_min_mip_from_pref(const char *value);
+// optimize RAW+JPEG import by extracting embedded JPEG and using it as base for
+// mipmap cache. extracts JPEG from RAW file, writes to mipmap level 8, then
+// downscales to create all mipmap tiers. returns TRUE if optimization was
+// applied, FALSE if JPEG extraction failed or feature disabled.
+gboolean dt_mipmap_cache_import_jpeg_to_mips(const dt_imgid_t imgid,
+ const char *filename);
+
G_END_DECLS
// clang-format off
diff --git a/src/control/jobs/camera_jobs.c b/src/control/jobs/camera_jobs.c
index 38102bd13043..a29eb62c060c 100644
--- a/src/control/jobs/camera_jobs.c
+++ b/src/control/jobs/camera_jobs.c
@@ -16,14 +16,15 @@
along with darktable. If not, see .
*/
#include "control/jobs/camera_jobs.h"
-#include "common/darktable.h"
#include "common/collection.h"
+#include "common/darktable.h"
+#include "common/datetime.h"
#include "common/import_session.h"
#include "common/utility.h"
-#include "common/datetime.h"
#include "control/conf.h"
#include "control/jobs/image_jobs.h"
#include "gui/gtk.h"
+#include "imageio/imageio_common.h"
#include "views/view.h"
#include
@@ -67,6 +68,11 @@ typedef struct dt_camera_import_t
dt_job_t *job;
double fraction;
uint32_t import_count;
+
+ gchar *last_imported_in_path;
+ gchar *last_imported_in_filename;
+ gchar *last_imported_path;
+
} dt_camera_import_t;
static int32_t dt_camera_capture_job_run(dt_job_t *job)
@@ -239,6 +245,46 @@ dt_job_t *dt_camera_capture_job_create(const char *jobcode,
return job;
}
+char *_find_companion_jpeg(const dt_camera_import_t *import,
+ const char *in_path, const char *in_filename) {
+ if (!in_filename || !in_path || !import)
+ return NULL;
+
+ char *base_noext = NULL;
+ char *dotpos = strrchr(in_filename, '.');
+ if (dotpos)
+ base_noext = g_strndup(in_filename, dotpos - in_filename);
+ else
+ base_noext = g_strdup(in_filename);
+
+ if (!dotpos || !dt_imageio_is_raw_by_extension(dotpos)) {
+ g_free(base_noext);
+ return NULL;
+ }
+
+ char *result = NULL;
+
+ if (import->last_imported_in_path && import->last_imported_path &&
+ import->last_imported_in_filename &&
+ g_strcmp0(import->last_imported_in_path, in_path) == 0) {
+ char *last_basename =
+ g_path_get_basename(import->last_imported_in_filename);
+ char *ldot = strrchr(last_basename, '.');
+ if (ldot)
+ *ldot = '\0';
+ if (g_strcmp0(last_basename, base_noext) == 0)
+ result = g_strdup(import->last_imported_path);
+ g_free(last_basename);
+ if (result) {
+ g_free(base_noext);
+ return result;
+ }
+ }
+
+ g_free(base_noext);
+ return result;
+}
+
/** Listener interface for import job */
void _camera_import_image_downloaded(const dt_camera_t *camera,
const char *in_path,
@@ -248,8 +294,30 @@ void _camera_import_image_downloaded(const dt_camera_t *camera,
{
// Import downloaded image to import filmroll
dt_camera_import_t *t = (dt_camera_import_t *)data;
+
+ // Find the preview files from the images list. Then check imported files by
+ // using Xmp.darktable.image_id. Then get its path to use for import preview
+ // image
+ char *preview_jpeg_file = NULL;
+ if (dt_conf_get_bool("cache/import_raw_jpeg_optimization"))
+ preview_jpeg_file = _find_companion_jpeg(t, in_path, in_filename);
+
const dt_imgid_t imgid =
- dt_image_import(dt_import_session_film_id(t->shared.session), filename, FALSE, TRUE);
+ dt_image_import(dt_import_session_film_id(t->shared.session), filename,
+ preview_jpeg_file, FALSE, TRUE);
+
+ /* Cache the last imported paths when we found a companion preview */
+ if (in_path != NULL && in_filename != NULL && dt_is_valid_imgid(imgid)) {
+ g_free(t->last_imported_in_path);
+ g_free(t->last_imported_in_filename);
+ g_free(t->last_imported_path);
+ t->last_imported_in_path = g_strdup(in_path);
+ t->last_imported_in_filename = g_strdup(in_filename);
+ t->last_imported_path = g_strdup(filename);
+ }
+
+ if (preview_jpeg_file != NULL)
+ g_free(preview_jpeg_file);
const time_t timestamp = (!in_path || !in_filename) ? 0 :
dt_camctl_get_image_file_timestamp(darktable.camctl, in_path, in_filename);
@@ -265,7 +333,7 @@ void _camera_import_image_downloaded(const dt_camera_t *camera,
dt_control_queue_redraw_center();
gchar *basename = g_path_get_basename(filename);
- const int num_images = g_list_length(t->images);
+ const int num_images = g_list_length(g_list_first(t->images));
dt_control_log(ngettext("%d/%d imported to %s", "%d/%d imported to %s",
t->import_count + 1),
t->import_count + 1, num_images, basename);
@@ -327,6 +395,18 @@ static int32_t dt_camera_import_job_run(dt_job_t *job)
dt_camera_import_t *params = dt_control_job_get_params(job);
dt_control_log(_("starting to import images from camera"));
+ /* Diagnostic: log received images list so we can debug missing items */
+ if (params && params->images) {
+ const guint nimg = g_list_length(params->images);
+ dt_control_log("camera import: images list length = %u", nimg);
+ for (GList *l = params->images; l; l = g_list_next(l)) {
+ if (l->data)
+ dt_control_log("camera import: entry: %s", (const char *)l->data);
+ else
+ dt_control_log("camera import: entry: (null)");
+ }
+ }
+
guint total = g_list_length(params->images);
dt_control_job_set_progress_message(job,
ngettext("importing %d image from camera",
@@ -370,6 +450,12 @@ static void dt_camera_import_cleanup(void *p)
dt_camera_import_t *params = p;
g_list_free(params->images);
+ g_free(params->last_imported_in_path);
+ g_free(params->last_imported_path);
+ g_free(params->last_imported_in_filename);
+ params->last_imported_in_path = NULL;
+ params->last_imported_path = NULL;
+ params->last_imported_in_filename = NULL;
dt_import_session_destroy(params->shared.session);
@@ -407,6 +493,9 @@ dt_job_t *dt_camera_import_job_create(GList *images,
params->camera = camera;
params->import_count = 0;
params->job = job;
+ params->last_imported_in_path = NULL;
+ params->last_imported_path = NULL;
+ params->last_imported_in_filename = NULL;
return job;
}
diff --git a/src/control/jobs/control_jobs.c b/src/control/jobs/control_jobs.c
index 705044e8a364..4152020b34f9 100644
--- a/src/control/jobs/control_jobs.c
+++ b/src/control/jobs/control_jobs.c
@@ -697,7 +697,8 @@ static int32_t _control_merge_hdr_job_run(dt_job_t *job)
gchar *directory = g_path_get_dirname((const gchar *)pathname);
dt_film_t film;
const dt_filmid_t filmid = dt_film_new(&film, directory);
- const dt_imgid_t imageid = dt_image_import(filmid, pathname, TRUE, TRUE);
+ const dt_imgid_t imageid =
+ dt_image_import(filmid, pathname, NULL, TRUE, TRUE);
g_free(directory);
// refresh the thumbtable view
@@ -2852,8 +2853,17 @@ static int _control_import_image_copy(const char *filename,
utimes(output, times); // set origin file timestamps
#endif
- const dt_imgid_t imgid = dt_image_import(dt_import_session_film_id(session),
- output, FALSE, FALSE);
+ /* attempt to also find a companion JPEG from the source directory
+ (same basename, .jpg or .jpeg, case-insensitive) into the
+ destination alongside the main file */
+ char *preview_jpeg_file = NULL;
+ if (dt_conf_get_bool("cache/import_raw_jpeg_optimization"))
+ preview_jpeg_file = dt_imageio_find_companion_jpeg(filename);
+
+ const dt_imgid_t imgid =
+ dt_image_import(dt_import_session_film_id(session), output,
+ preview_jpeg_file, FALSE, FALSE);
+ g_free(preview_jpeg_file);
if(!dt_is_valid_imgid(imgid)) dt_control_log(_("error loading file `%s'"), output);
else
{
@@ -2908,7 +2918,12 @@ static int _control_import_image_insitu(const char *filename,
char *dirname = dt_util_path_get_dirname(filename);
dt_film_t film;
const dt_filmid_t filmid = dt_film_new(&film, dirname);
- const dt_imgid_t imgid = dt_image_import(filmid, filename, FALSE, FALSE);
+ char *preview_jpeg_file = NULL;
+ if (dt_conf_get_bool("cache/import_raw_jpeg_optimization"))
+ preview_jpeg_file = dt_imageio_find_companion_jpeg(filename);
+ const dt_imgid_t imgid =
+ dt_image_import(filmid, filename, preview_jpeg_file, FALSE, FALSE);
+ g_free(preview_jpeg_file);
if(!dt_is_valid_imgid(imgid)) dt_control_log(_("error loading file `%s'"), filename);
else
{
diff --git a/src/control/jobs/film_jobs.c b/src/control/jobs/film_jobs.c
index fbadc1854b7d..c1a96ee1c2c6 100644
--- a/src/control/jobs/film_jobs.c
+++ b/src/control/jobs/film_jobs.c
@@ -333,7 +333,8 @@ static void _film_import1(dt_job_t *job, dt_film_t *film, GList *images)
g_free(cdn);
/* import image */
- const dt_imgid_t imgid = dt_image_import(cfr->id, (const gchar *)image->data, FALSE, FALSE);
+ const dt_imgid_t imgid = dt_image_import(
+ cfr->id, (const gchar *)image->data, NULL, FALSE, FALSE);
pending++; // we have another image which hasn't been reported yet
fraction += 1.0 / total;
dt_control_job_set_progress(job, fraction);
diff --git a/src/control/jobs/image_jobs.c b/src/control/jobs/image_jobs.c
index 4f7b45d5b687..649ad5bb39c7 100644
--- a/src/control/jobs/image_jobs.c
+++ b/src/control/jobs/image_jobs.c
@@ -74,7 +74,8 @@ static int32_t _image_import_job_run(dt_job_t *job)
dt_control_job_set_progress_message(job, _("importing image %s"), params->filename);
- const dt_imgid_t id = dt_image_import(params->film_id, params->filename, TRUE, TRUE);
+ const dt_imgid_t id =
+ dt_image_import(params->film_id, params->filename, NULL, TRUE, TRUE);
if(dt_is_valid_imgid(id))
{
DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, id);
diff --git a/src/imageio/imageio.c b/src/imageio/imageio.c
index dc20eed1cd56..2e7d77f32601 100644
--- a/src/imageio/imageio.c
+++ b/src/imageio/imageio.c
@@ -521,6 +521,9 @@ static const gchar *_supported_ldr[]
NULL };
static const gchar *_supported_hdr[]
= { "avif", "exr", "hdr", "heic", "heif", "hif", "jxl", "pfm", NULL };
+static const gchar *_supported_raw_preview[] = {"jpg", "jpeg", "j2k", "j2c",
+ "jpe", "jiff", "thm", "tif",
+ "tiff", "png", "bmp", NULL};
static inline gboolean _image_handled(dt_imageio_retval_t ret)
{
@@ -622,6 +625,63 @@ gboolean dt_imageio_is_raw_by_extension(const char *extension)
return FALSE;
}
+gboolean dt_imageio_is_raw_preview_by_extension(const char *extension) {
+ const char *ext =
+ g_str_has_prefix(extension, ".") ? extension + 1 : extension;
+ for (const char **i = _supported_raw_preview; *i != NULL; i++) {
+ if (!g_ascii_strcasecmp(ext, *i))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+char *dt_imageio_find_companion_jpeg(const char *filepath) {
+ if (!filepath)
+ return NULL;
+
+ char *src_dir = g_path_get_dirname(filepath);
+ char *src_basename = g_path_get_basename(filepath);
+ char *dotpos = strrchr(src_basename, '.');
+ char *base_noext = NULL;
+ if (dotpos)
+ base_noext = g_strndup(src_basename, dotpos - src_basename);
+ else
+ base_noext = g_strdup(src_basename);
+
+ char *result = NULL;
+
+ GDir *d = g_dir_open(src_dir, 0, NULL);
+ if (d) {
+ const char *entry;
+ while ((entry = g_dir_read_name(d)) != NULL) {
+ const char *edot = strrchr(entry, '.');
+ if (!edot)
+ continue;
+ const size_t name_len = edot - entry;
+ if (name_len != strlen(base_noext))
+ continue;
+ if (g_ascii_strncasecmp(entry, base_noext, name_len) != 0)
+ continue;
+ if (dt_imageio_is_raw_preview_by_extension(edot)) {
+ char *cand = g_build_filename(src_dir, entry, NULL);
+ if (g_file_test(cand, G_FILE_TEST_IS_REGULAR)) {
+ result = g_strdup(cand);
+ g_free(cand);
+ break;
+ }
+ g_free(cand);
+ }
+ }
+ g_dir_close(d);
+ }
+
+ g_free(src_dir);
+ g_free(src_basename);
+ g_free(base_noext);
+
+ return result;
+}
+
// get the type of image from its extension
dt_image_flags_t dt_imageio_get_type_from_extension(const char *extension)
{
diff --git a/src/imageio/imageio_common.h b/src/imageio/imageio_common.h
index 2364623f5906..1e2dbc0d204a 100644
--- a/src/imageio/imageio_common.h
+++ b/src/imageio/imageio_common.h
@@ -58,6 +58,12 @@ typedef enum dt_imageio_levels_t
// Check that the image is raw by file extension
gboolean dt_imageio_is_raw_by_extension(const char *extension);
+// Check that the image is raw preview by file extension
+gboolean dt_imageio_is_raw_preview_by_extension(const char *extension);
+// Find a companion JPEG (same basename, .jpg/.jpeg) in the same directory as
+// `filepath`. Returns a newly-allocated string with the full path, or NULL if
+// not found.
+char *dt_imageio_find_companion_jpeg(const char *filepath);
// Checks that the image is indeed an ldr image
gboolean dt_imageio_is_ldr(const char *filename);
// checks that the image has a monochrome preview attached
diff --git a/src/lua/database.c b/src/lua/database.c
index 536c11c703e3..b7493169a42a 100644
--- a/src/lua/database.c
+++ b/src/lua/database.c
@@ -168,7 +168,7 @@ static int import_images(lua_State *L)
return luaL_error(L, "error while importing");
}
- result = dt_image_import_lua(new_film.id, full_name, TRUE);
+ result = dt_image_import_lua(new_film.id, full_name, NULL, TRUE);
if(dt_film_is_empty(new_film.id)) dt_film_remove(new_film.id);
dt_film_cleanup(&new_film);
if(!dt_is_valid_filmid(result))