Пример #1
0
int dt_load_from_string(const gchar* input, gboolean open_image_in_dr)
{
  int id = 0;
  if(input == NULL || input[0] == '\0')
    return 0;

  char* filename = dt_make_path_absolute(input);

  if(filename == NULL)
  {
    dt_control_log(_("found strange path `%s'"), input);
    return 0;
  }

  if(g_file_test(filename, G_FILE_TEST_IS_DIR))
  {
    // import a directory into a film roll
    unsigned int last_char = strlen(filename)-1;
    if(filename[last_char] == '/')
      filename[last_char] = '\0';
    id = dt_film_import(filename);
    if(id)
    {
      dt_film_open(id);
      dt_ctl_switch_mode_to(DT_LIBRARY);
    }
    else
    {
      dt_control_log(_("error loading directory `%s'"), filename);
    }
  }
  else
  {
    // import a single image
    gchar *directory = g_path_get_dirname((const gchar *)filename);
    dt_film_t film;
    const int filmid = dt_film_new(&film, directory);
    id = dt_image_import(filmid, filename, TRUE);
    g_free (directory);
    if(id)
    {
      dt_film_open(filmid);
      // make sure buffers are loaded (load full for testing)
      dt_mipmap_buffer_t buf;
      dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, id, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);
      if(!buf.buf)
      {
        id = 0;
        dt_control_log(_("file `%s' has unknown format!"), filename);
      }
      else
      {
        dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
        if(open_image_in_dr)
        {
          dt_control_set_mouse_over_id(id);
          dt_ctl_switch_mode_to(DT_DEVELOP);
        }
      }
    }
    else
    {
      dt_control_log(_("error loading file `%s'"), filename);
    }
  }
  g_free(filename);
  return id;
}
Пример #2
0
static void
_lib_filmstrip_dnd_begin_callback(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
{
  const int ts = 64;

  dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  dt_lib_filmstrip_t *strip = (dt_lib_filmstrip_t *)self->data;

  int imgid = strip->mouse_over_id;

  // imgid part of selection -> do nothing
  // otherwise               -> select the current image
  strip->select = DT_LIB_FILMSTRIP_SELECT_NONE;
  sqlite3_stmt *stmt;
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "select imgid from selected_images where imgid=?1", -1, &stmt, NULL);
  DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, imgid);
  if(sqlite3_step(stmt) != SQLITE_ROW)
  {
    dt_selection_select_single(darktable.selection, imgid);
    /* redraw filmstrip */
    if(darktable.view_manager->proxy.filmstrip.module)
      gtk_widget_queue_draw(darktable.view_manager->proxy.filmstrip.module->widget);
  }
  sqlite3_finalize(stmt);

  // if we are dragging a single image -> use the thumbnail of that image
  // otherwise use the generic d&d icon
  // TODO: have something pretty in the 2nd case, too.
  if(dt_collection_get_selected_count(NULL) == 1)
  {
    dt_mipmap_buffer_t buf;
    dt_mipmap_size_t mip = dt_mipmap_cache_get_matching_size(darktable.mipmap_cache, ts, ts);
    dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, imgid, mip, DT_MIPMAP_BLOCKING);

    if(buf.buf)
    {
      uint8_t *scratchmem = dt_mipmap_cache_alloc_scratchmem(darktable.mipmap_cache);
      uint8_t *buf_decompressed = dt_mipmap_cache_decompress(&buf, scratchmem);

      uint8_t *rgbbuf = g_malloc((buf.width+2)*(buf.height+2)*3);
      memset(rgbbuf, 64, (buf.width+2)*(buf.height+2)*3);
      for(int i=1; i<=buf.height; i++)
        for(int j=1; j<=buf.width; j++)
          for(int k=0; k<3; k++)
            rgbbuf[(i*(buf.width+2)+j)*3+k] = buf_decompressed[((i-1)*buf.width+j-1)*4+2-k];

      int w=ts, h=ts;
      if(buf.width < buf.height) w = (buf.width*ts)/buf.height; // portrait
      else                       h = (buf.height*ts)/buf.width; // landscape

      GdkPixbuf *source = gdk_pixbuf_new_from_data(rgbbuf, GDK_COLORSPACE_RGB, FALSE, 8, (buf.width+2), (buf.height+2), (buf.width+2)*3, NULL, NULL);
      GdkPixbuf *scaled = gdk_pixbuf_scale_simple(source, w, h, GDK_INTERP_HYPER);
      gtk_drag_set_icon_pixbuf(context, scaled, 0, 0);

      if(source)
        g_object_unref(source);
      if(scaled)
        g_object_unref(scaled);
      g_free(rgbbuf);
    }

    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
  }
}
Пример #3
0
int32_t dt_control_merge_hdr_job_run(dt_job_t *job)
{
  int imgid = -1;
  dt_control_image_enumerator_t *t1 = (dt_control_image_enumerator_t *)job->param;
  GList *t = t1->index;
  int total = g_list_length(t);
  char message[512]= {0};
  double fraction=0;
  snprintf(message, 512, ngettext ("merging %d image", "merging %d images", total), total );

  const guint *jid = dt_control_backgroundjobs_create(darktable.control, 1, message);

  float *pixels = NULL;
  float *weight = NULL;
  int wd = 0, ht = 0, first_imgid = -1;
  uint32_t filter = 0;
  float whitelevel = 0.0f;
  total ++;
  while(t)
  {
    imgid = GPOINTER_TO_INT(t->data);
    dt_mipmap_buffer_t buf;
    dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, imgid, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);
    // just take a copy. also do it after blocking read, so filters and bpp will make sense.
    const dt_image_t *img = dt_image_cache_read_get(darktable.image_cache, imgid);
    dt_image_t image = *img;
    dt_image_cache_read_release(darktable.image_cache, img);
    if(image.filters == 0 || image.bpp != sizeof(uint16_t))
    {
      dt_control_log(_("exposure bracketing only works on raw images"));
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      free(pixels);
      free(weight);
      goto error;
    }
    filter = dt_image_flipped_filter(img);
    if(buf.size != DT_MIPMAP_FULL)
    {
      dt_control_log(_("failed to get raw buffer from image `%s'"), image.filename);
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      free(pixels);
      free(weight);
      goto error;
    }

    if(!pixels)
    {
      first_imgid = imgid;
      pixels = (float *)malloc(sizeof(float)*image.width*image.height);
      weight = (float *)malloc(sizeof(float)*image.width*image.height);
      memset(pixels, 0x0, sizeof(float)*image.width*image.height);
      memset(weight, 0x0, sizeof(float)*image.width*image.height);
      wd = image.width;
      ht = image.height;
    }
    else if(image.width != wd || image.height != ht)
    {
      dt_control_log(_("images have to be of same size!"));
      free(pixels);
      free(weight);
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      goto error;
    }
    // if no valid exif data can be found, assume peleng fisheye at f/16, 8mm, with half of the light lost in the system => f/22
    const float eap = image.exif_aperture > 0.0f ? image.exif_aperture : 22.0f;
    const float efl = image.exif_focal_length > 0.0f ? image.exif_focal_length : 8.0f;
    const float rad = .5f * efl/eap;
    const float aperture = M_PI * rad * rad;
    const float iso = image.exif_iso > 0.0f ? image.exif_iso : 100.0f;
    const float exp = image.exif_exposure > 0.0f ? image.exif_exposure : 1.0f;
    const float cal = 100.0f/(aperture*exp*iso);
    // about proportional to how many photons we can expect from this shot:
    const float photoncnt = 100.0f*aperture*exp/iso;
    // stupid, but we don't know the real sensor saturation level:
    uint16_t saturation = 0;
    for(int k=0; k<wd*ht; k++)
      saturation = MAX(saturation, ((uint16_t *)buf.buf)[k]);
    // seems to be around 64500--64700 for 5dm2
    // fprintf(stderr, "saturation: %u\n", saturation);
    whitelevel = fmaxf(whitelevel, saturation*cal);
#ifdef _OPENMP
    #pragma omp parallel for schedule(static) default(none) shared(buf, pixels, weight, wd, ht, saturation)
#endif
    for(int k=0; k<wd*ht; k++)
    {
      const uint16_t in = ((uint16_t *)buf.buf)[k];
      // weights based on siggraph 12 poster
      // zijian zhu, zhengguo li, susanto rahardja, pasi fraenti
      // 2d denoising factor for high dynamic range imaging
      float w = envelope(in/(float)saturation) * photoncnt;
      // in case we are black and drop to zero weight, give it something
      // just so numerics don't collapse. blown out whites are handled below.
      if(w < 1e-3f && in < saturation/3) w = 1e-3f;
      pixels[k] += w * in * cal;
      weight[k] += w;
    }

    t = g_list_delete_link(t, t);

    /* update backgroundjob ui plate */
    fraction+=1.0/total;
    dt_control_backgroundjobs_progress(darktable.control, jid, fraction);

    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
  }
  // normalize by white level to make clipping at 1.0 work as expected (to be sure, scale down one more stop, thus the 0.5):
#ifdef _OPENMP
  #pragma omp parallel for schedule(static) default(none) shared(pixels, wd, ht, weight, whitelevel)
#endif
  for(int k=0; k<wd*ht; k++)
  {
    // in case w == 0, all pixels were overexposed (too dark would have been clamped to w >= eps above)
    if(weight[k] < 1e-3f)
      pixels[k] = 1.f; // mark as blown out.
    else // normalize:
      pixels[k] = fmaxf(0.0f, pixels[k]/(whitelevel*weight[k]));
  }

  // output hdr as digital negative with exif data.
  uint8_t exif[65535];
  char pathname[DT_MAX_PATH_LEN];
  gboolean from_cache = TRUE;
  dt_image_full_path(first_imgid, pathname, DT_MAX_PATH_LEN, &from_cache);
  // last param is dng mode
  const int exif_len = dt_exif_read_blob(exif, pathname, first_imgid, 0, wd, ht, 1);
  char *c = pathname + strlen(pathname);
  while(*c != '.' && c > pathname) c--;
  g_strlcpy(c, "-hdr.dng", sizeof(pathname)-(c-pathname));
  dt_imageio_write_dng(pathname, pixels, wd, ht, exif, exif_len, filter, 1.0f);

  dt_control_backgroundjobs_progress(darktable.control, jid, 1.0f);

  while(*c != '/' && c > pathname) c--;
  dt_control_log(_("wrote merged HDR `%s'"), c+1);

  // import new image
  gchar *directory = g_path_get_dirname((const gchar *)pathname);
  dt_film_t film;
  const int filmid = dt_film_new(&film, directory);
  dt_image_import(filmid, pathname, TRUE);
  g_free (directory);

  free(pixels);
  free(weight);
error:
  dt_control_backgroundjobs_destroy(darktable.control, jid);
  dt_control_queue_redraw_center();
  return 0;
}
Пример #4
0
static void _lib_import_single_image_callback(GtkWidget *widget,gpointer user_data) 
{
  GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
  GtkWidget *filechooser = gtk_file_chooser_dialog_new (_("import image"),
                           GTK_WINDOW (win),
                           GTK_FILE_CHOOSER_ACTION_OPEN,
                           GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                           GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
                           (char *)NULL);

  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), TRUE);

  char *last_directory = dt_conf_get_string("ui_last/import_last_directory");
  if(last_directory != NULL)
    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER (filechooser), last_directory);

  char *cp, **extensions, ext[1024];
  GtkFileFilter *filter;
  filter = GTK_FILE_FILTER(gtk_file_filter_new());
  extensions = g_strsplit(dt_supported_extensions, ",", 100);
  for(char **i=extensions; *i!=NULL; i++)
  {
    snprintf(ext, 1024, "*.%s", *i);
    gtk_file_filter_add_pattern(filter, ext);
    gtk_file_filter_add_pattern(filter, cp=g_ascii_strup(ext, -1));
    g_free(cp);
  }
  g_strfreev(extensions);
  gtk_file_filter_set_name(filter, _("supported images"));
  gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);

  filter = GTK_FILE_FILTER(gtk_file_filter_new());
  gtk_file_filter_add_pattern(filter, "*");
  gtk_file_filter_set_name(filter, _("all files"));
  gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);

  if (gtk_dialog_run (GTK_DIALOG (filechooser)) == GTK_RESPONSE_ACCEPT)
  {
    dt_conf_set_string("ui_last/import_last_directory", gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER (filechooser)));

    char *filename = NULL;
    dt_film_t film;
    GSList *list = gtk_file_chooser_get_filenames (GTK_FILE_CHOOSER (filechooser));
    GSList *it = list;
    int id = 0;
    int filmid = 0;
    while(it)
    {
      filename = (char *)it->data;
      gchar *directory = g_path_get_dirname((const gchar *)filename);
      filmid = dt_film_new(&film, directory);
      id = dt_image_import(filmid, filename, TRUE);
      if(!id) dt_control_log(_("error loading file `%s'"), filename);
      g_free (filename);
      g_free (directory);
      it = g_slist_next(it);
    }

    if(id)
    {
      dt_film_open(filmid);
      // make sure buffers are loaded (load full for testing)
      dt_mipmap_buffer_t buf;
      dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, id, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);
      if(!buf.buf)
      {
        dt_control_log(_("file has unknown format!"));
      }
      else
      {
        dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
        DT_CTL_SET_GLOBAL(lib_image_mouse_over_id, id);
        dt_ctl_switch_mode_to(DT_DEVELOP);
      }
    }
  }
  gtk_widget_destroy (filechooser);
  gtk_widget_queue_draw(dt_ui_center(darktable.gui->ui));
}
Пример #5
0
int32_t dt_control_merge_hdr_job_run(dt_job_t *job)
{
  long int imgid = -1;
  dt_control_image_enumerator_t *t1 = (dt_control_image_enumerator_t *)job->param;
  GList *t = t1->index;
  int total = g_list_length(t);
  char message[512]= {0};
  double fraction=0;
  snprintf(message, 512, ngettext ("merging %d image", "merging %d images", total), total );

  const guint *jid = dt_control_backgroundjobs_create(darktable.control, 1, message); 
 
  float *pixels = NULL;
  float *weight = NULL;
  int wd = 0, ht = 0, first_imgid = -1;
  uint32_t filter = 0;
  float whitelevel = 0.0f;
  total ++;
  while(t)
  {
    imgid = (long int)t->data;
    dt_mipmap_buffer_t buf;
    dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, imgid, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);
    // just take a copy. also do it after blocking read, so filters and bpp will make sense.
    const dt_image_t *img = dt_image_cache_read_get(darktable.image_cache, imgid);
    dt_image_t image = *img;
    dt_image_cache_read_release(darktable.image_cache, img);
    if(image.filters == 0 || image.bpp != sizeof(uint16_t))
    {
      dt_control_log(_("exposure bracketing only works on raw images"));
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      free(pixels);
      free(weight);
      goto error;
    }
    filter = dt_image_flipped_filter(img);
    if(buf.size != DT_MIPMAP_FULL)
    {
      dt_control_log(_("failed to get raw buffer from image `%s'"), image.filename);
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      free(pixels);
      free(weight);
      goto error;
    }

    if(!pixels)
    {
      first_imgid = imgid;
      pixels = (float *)malloc(sizeof(float)*image.width*image.height);
      weight = (float *)malloc(sizeof(float)*image.width*image.height);
      memset(pixels, 0x0, sizeof(float)*image.width*image.height);
      memset(weight, 0x0, sizeof(float)*image.width*image.height);
      wd = image.width;
      ht = image.height;
    }
    else if(image.width != wd || image.height != ht)
    {
      dt_control_log(_("images have to be of same size!"));
      free(pixels);
      free(weight);
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      goto error;
    }
    // if no valid exif data can be found, assume peleng fisheye at f/16, 8mm, with half of the light lost in the system => f/22
    const float eap = image.exif_aperture > 0.0f ? image.exif_aperture : 22.0f;
    const float efl = image.exif_focal_length > 0.0f ? image.exif_focal_length : 8.0f;
    const float rad = .5f * efl/eap;
    const float aperture = M_PI * rad * rad;
    const float iso = image.exif_iso > 0.0f ? image.exif_iso : 100.0f;
    const float exp = image.exif_exposure > 0.0f ? image.exif_exposure : 1.0f;
    const float cal = 100.0f/(aperture*exp*iso);
    whitelevel = fmaxf(whitelevel, cal);
#ifdef _OPENMP
    #pragma omp parallel for schedule(static) default(none) shared(buf, pixels, weight, wd, ht)
#endif
    for(int k=0; k<wd*ht; k++)
    {
      const uint16_t in = ((uint16_t *)buf.buf)[k];
      const float w = .001f + (in >= 1000 ? (in < 65000 ? in/65000.0f : 0.0f) : exp * 0.01f);
      pixels[k] += w * in * cal;
      weight[k] += w;
    }

    t = g_list_delete_link(t, t);
    
    /* update backgroundjob ui plate */
    fraction+=1.0/total;
    dt_control_backgroundjobs_progress(darktable.control, jid, fraction);

    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
  }
  // normalize by white level to make clipping at 1.0 work as expected (to be sure, scale down one more stop, thus the 0.5):
#ifdef _OPENMP
  #pragma omp parallel for schedule(static) default(none) shared(pixels, wd, ht, weight, whitelevel)
#endif
  for(int k=0; k<wd*ht; k++) pixels[k] = fmaxf(0.0f, fminf(2.0f, pixels[k]/((.5f*whitelevel*65535.0f)*weight[k])));

  // output hdr as digital negative with exif data.
  uint8_t exif[65535];
  char pathname[1024];
  dt_image_full_path(first_imgid, pathname, 1024);
  const int exif_len = dt_exif_read_blob(exif, pathname, 0, first_imgid);
  char *c = pathname + strlen(pathname);
  while(*c != '.' && c > pathname) c--;
  g_strlcpy(c, "-hdr.dng", sizeof(pathname)-(c-pathname));
  dt_imageio_write_dng(pathname, pixels, wd, ht, exif, exif_len, filter, whitelevel);
  
  dt_control_backgroundjobs_progress(darktable.control, jid, 1.0f);

  while(*c != '/' && c > pathname) c--;
  dt_control_log(_("wrote merged hdr `%s'"), c+1);

  // import new image
  gchar *directory = g_path_get_dirname((const gchar *)pathname);
  dt_film_t film;
  const int filmid = dt_film_new(&film, directory);
  dt_image_import(filmid, pathname, TRUE);
  g_free (directory);

  free(pixels);
  free(weight);
error:
  dt_control_backgroundjobs_destroy(darktable.control, jid);
  return 0;
}
Пример #6
0
static void
_init_f(
  float          *out,
  uint32_t       *width,
  uint32_t       *height,
  const uint32_t  imgid)
{
  const uint32_t wd = *width, ht = *height;

  /* do not even try to process file if it isn't available */
  char filename[2048] = {0};
  gboolean from_cache = TRUE;
  dt_image_full_path(imgid, filename, sizeof(filename), &from_cache);
  if ( !*filename || !g_file_test(filename, G_FILE_TEST_EXISTS))
  {
    *width = *height = 0;
    return;
  }

  dt_mipmap_buffer_t buf;
  dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, imgid, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);

  // lock image after we have the buffer, we might need to lock the image struct for
  // writing during raw loading, to write to width/height.
  const dt_image_t *image = dt_image_cache_read_get(darktable.image_cache, imgid);

  dt_iop_roi_t roi_in, roi_out;
  roi_in.x = roi_in.y = 0;
  roi_in.width = image->width;
  roi_in.height = image->height;
  roi_in.scale = 1.0f;

  roi_out.x = roi_out.y = 0;
  roi_out.scale = fminf(wd/(float)image->width, ht/(float)image->height);
  roi_out.width  = roi_out.scale * roi_in.width;
  roi_out.height = roi_out.scale * roi_in.height;

  if(!buf.buf)
  {
    dt_control_log(_("image `%s' is not available!"), image->filename);
    dt_image_cache_read_release(darktable.image_cache, image);
    *width = *height = 0;
    return;
  }

  assert(!buffer_is_broken(&buf));

  if(image->filters)
  {
    // demosaic during downsample
    if(image->bpp == sizeof(float))
      dt_iop_clip_and_zoom_demosaic_half_size_f(
        out, (const float *)buf.buf,
        &roi_out, &roi_in, roi_out.width, roi_in.width,
        dt_image_flipped_filter(image), 1.0f);
    else
      dt_iop_clip_and_zoom_demosaic_half_size(
        out, (const uint16_t *)buf.buf,
        &roi_out, &roi_in, roi_out.width, roi_in.width,
        dt_image_flipped_filter(image));
  }
  else
  {
    // downsample
    dt_iop_clip_and_zoom(out, (const float *)buf.buf,
                         &roi_out, &roi_in, roi_out.width, roi_in.width);
  }
  dt_image_cache_read_release(darktable.image_cache, image);
  dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);

  *width  = roi_out.width;
  *height = roi_out.height;
}
Пример #7
0
// internal function: to avoid exif blob reading + 8-bit byteorder flag + high-quality override
int dt_imageio_export_with_flags(
  const uint32_t              imgid,
  const char                 *filename,
  dt_imageio_module_format_t *format,
  dt_imageio_module_data_t   *format_params,
  const int32_t               ignore_exif,
  const int32_t               display_byteorder,
  const gboolean              high_quality,
  const int32_t               thumbnail_export,
  const char                 *filter)
{
  dt_develop_t dev;
  dt_dev_init(&dev, 0);
  dt_mipmap_buffer_t buf;
  if(thumbnail_export && dt_conf_get_bool("plugins/lighttable/low_quality_thumbnails"))
    dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, imgid, DT_MIPMAP_F, DT_MIPMAP_BLOCKING);
  else
    dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, imgid, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);
  dt_dev_load_image(&dev, imgid);
  const dt_image_t *img = &dev.image_storage;
  const int wd = img->width;
  const int ht = img->height;

  int res = 0;

  dt_times_t start;
  dt_get_times(&start);
  dt_dev_pixelpipe_t pipe;
  res = thumbnail_export ? dt_dev_pixelpipe_init_thumbnail(&pipe, wd, ht) : dt_dev_pixelpipe_init_export(&pipe, wd, ht, format->levels(format_params));
  if(!res)
  {
    dt_control_log(_("failed to allocate memory for export, please lower the threads used for export or buy more memory."));
    dt_dev_cleanup(&dev);
    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
    return 1;
  }

  if(!buf.buf)
  {
    dt_control_log(_("image `%s' is not available!"), img->filename);
    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
    dt_dev_cleanup(&dev);
    return 1;
  }

  //  If a style is to be applied during export, add the iop params into the history
  if (!thumbnail_export && strlen(format_params->style) && strcmp(format_params->style,_("none")))
  {
    GList *stls;

    GList *modules = dev.iop;
    dt_iop_module_t *m = NULL;

    if ((stls=dt_styles_get_item_list(format_params->style, TRUE)) == 0)
    {
      dt_control_log(_("cannot find the style '%s' to apply during export."), format_params->style);
      dt_dev_cleanup(&dev);
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      return 1;
    }

    //  Add each params
    while (stls)
    {
      dt_style_item_t *s = (dt_style_item_t *) stls->data;

      modules = dev.iop;
      while (modules)
      {
        m = (dt_iop_module_t *)modules->data;

        if (strcmp(m->op, s->name) == 0)
        {
          dt_dev_history_item_t *h = malloc(sizeof(dt_dev_history_item_t));

          h->params = s->params;
          h->enabled = 1;
          h->module = m;
          h->multi_priority = 1;
          strcpy(h->multi_name, "");

          h->blend_params = malloc(sizeof(dt_develop_blend_params_t));
          memset(h->blend_params, 0, sizeof(dt_develop_blend_params_t));

          dev.history_end++;
          dev.history = g_list_append(dev.history, h);
          break;
        }
        modules = g_list_next(modules);
      }
      stls = g_list_next(stls);
    }
  }

  dt_dev_pixelpipe_set_input(&pipe, &dev, (float *)buf.buf, buf.width, buf.height, 1.0);
  dt_dev_pixelpipe_create_nodes(&pipe, &dev);
  dt_dev_pixelpipe_synch_all(&pipe, &dev);
  dt_dev_pixelpipe_get_dimensions(&pipe, &dev, pipe.iwidth, pipe.iheight, &pipe.processed_width, &pipe.processed_height);
  if(filter)
  {
    if(!strncmp(filter, "pre:", 4))
      dt_dev_pixelpipe_disable_after(&pipe, filter+4);
    if(!strncmp(filter, "post:", 5))
      dt_dev_pixelpipe_disable_before(&pipe, filter+5);
  }
  dt_show_times(&start, "[export] creating pixelpipe", NULL);

  // find output color profile for this image:
  int sRGB = 1;
  gchar *overprofile = dt_conf_get_string("plugins/lighttable/export/iccprofile");
  if(overprofile && !strcmp(overprofile, "sRGB"))
  {
    sRGB = 1;
  }
  else if(!overprofile || !strcmp(overprofile, "image"))
  {
    GList *modules = dev.iop;
    dt_iop_module_t *colorout = NULL;
    while (modules)
    {
      colorout = (dt_iop_module_t *)modules->data;
      if (strcmp(colorout->op, "colorout") == 0)
      {
        dt_iop_colorout_params_t *p = (dt_iop_colorout_params_t *)colorout->params;
        if(!strcmp(p->iccprofile, "sRGB")) sRGB = 1;
        else sRGB = 0;
      }
      modules = g_list_next(modules);
    }
  }
  else
  {
    sRGB = 0;
  }
  g_free(overprofile);

  // get only once at the beginning, in case the user changes it on the way:
  const gboolean high_quality_processing = ((format_params->max_width  == 0 || format_params->max_width  >= pipe.processed_width ) &&
                                       (format_params->max_height == 0 || format_params->max_height >= pipe.processed_height)) ? FALSE :
                                      high_quality;
  const int width  = high_quality_processing ? 0 : format_params->max_width;
  const int height = high_quality_processing ? 0 : format_params->max_height;
  const double scalex = width  > 0 ? fminf(width /(double)pipe.processed_width,  1.0) : 1.0;
  const double scaley = height > 0 ? fminf(height/(double)pipe.processed_height, 1.0) : 1.0;
  const double scale = fminf(scalex, scaley);
  int processed_width  = scale*pipe.processed_width  + .5f;
  int processed_height = scale*pipe.processed_height + .5f;
  const int bpp = format->bpp(format_params);

  // downsampling done last, if high quality processing was requested:
  uint8_t *outbuf = pipe.backbuf;
  uint8_t *moutbuf = NULL; // keep track of alloc'ed memory
  if(high_quality_processing)
  {
    dt_dev_pixelpipe_process_no_gamma(&pipe, &dev, 0, 0, processed_width, processed_height, scale);
    const double scalex = format_params->max_width  > 0 ? fminf(format_params->max_width /(double)pipe.processed_width,  1.0) : 1.0;
    const double scaley = format_params->max_height > 0 ? fminf(format_params->max_height/(double)pipe.processed_height, 1.0) : 1.0;
    const double scale = fminf(scalex, scaley);
    processed_width  = scale*pipe.processed_width  + .5f;
    processed_height = scale*pipe.processed_height + .5f;
    moutbuf = (uint8_t *)dt_alloc_align(64, sizeof(float)*processed_width*processed_height*4);
    outbuf = moutbuf;
    // now downscale into the new buffer:
    dt_iop_roi_t roi_in, roi_out;
    roi_in.x = roi_in.y = roi_out.x = roi_out.y = 0;
    roi_in.scale = 1.0;
    roi_out.scale = scale;
    roi_in.width = pipe.processed_width;
    roi_in.height = pipe.processed_height;
    roi_out.width = processed_width;
    roi_out.height = processed_height;
    dt_iop_clip_and_zoom((float *)outbuf, (float *)pipe.backbuf, &roi_out, &roi_in, processed_width, pipe.processed_width);
  }
  else
  {
    // do the processing (8-bit with special treatment, to make sure we can use openmp further down):
    if(bpp == 8)
      dt_dev_pixelpipe_process(&pipe, &dev, 0, 0, processed_width, processed_height, scale);
    else
      dt_dev_pixelpipe_process_no_gamma(&pipe, &dev, 0, 0, processed_width, processed_height, scale);
    outbuf = pipe.backbuf;
  }

  // downconversion to low-precision formats:
  if(bpp == 8 && !display_byteorder)
  {
    // ldr output: char
    if(high_quality_processing)
    {
      const float *const inbuf = (float *)outbuf;
      for(int k=0; k<processed_width*processed_height; k++)
      {
        // convert in place, this is unfortunately very serial..
        const uint8_t r = CLAMP(inbuf[4*k+0]*0xff, 0, 0xff);
        const uint8_t g = CLAMP(inbuf[4*k+1]*0xff, 0, 0xff);
        const uint8_t b = CLAMP(inbuf[4*k+2]*0xff, 0, 0xff);
        outbuf[4*k+0] = r;
        outbuf[4*k+1] = g;
        outbuf[4*k+2] = b;
      }
    }
    else
    {
      uint8_t *const buf8 = pipe.backbuf;
#ifdef _OPENMP
      #pragma omp parallel for default(none) shared(processed_width, processed_height) schedule(static)
#endif
      // just flip byte order
      for(int k=0; k<processed_width*processed_height; k++)
      {
        uint8_t tmp = buf8[4*k+0];
        buf8[4*k+0] = buf8[4*k+2];
        buf8[4*k+2] = tmp;
      }
    }
  }
  else if(bpp == 16)
  {
    // uint16_t per color channel
    float    *buff  = (float *)   outbuf;
    uint16_t *buf16 = (uint16_t *)outbuf;
    for(int y=0; y<processed_height; y++) for(int x=0; x<processed_width ; x++)
      {
        // convert in place
        const int k = x + processed_width*y;
        for(int i=0; i<3; i++) buf16[4*k+i] = CLAMP(buff[4*k+i]*0x10000, 0, 0xffff);
      }
  }
  // else output float, no further harm done to the pixels :)

  format_params->width  = processed_width;
  format_params->height = processed_height;

  if(!ignore_exif)
  {
    int length;
    uint8_t exif_profile[65535]; // C++ alloc'ed buffer is uncool, so we waste some bits here.
    char pathname[1024];
    dt_image_full_path(imgid, pathname, 1024);
    length = dt_exif_read_blob(exif_profile, pathname, imgid, sRGB, processed_width, processed_height);

    res = format->write_image (format_params, filename, outbuf, exif_profile, length, imgid);
  }
  else
  {
    res = format->write_image (format_params, filename, outbuf, NULL, 0, imgid);
  }

  dt_dev_pixelpipe_cleanup(&pipe);
  dt_dev_cleanup(&dev);
  dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
  free(moutbuf);
  return res;
}
Пример #8
0
void dt_dev_process_image_job(dt_develop_t *dev)
{
  dt_control_log_busy_enter();
  // let gui know to draw preview instead of us, if it's there:
  dev->image_dirty = 1;

  dt_mipmap_buffer_t buf;
  dt_times_t start;
  dt_get_times(&start);
  dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, dev->image_storage.id, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING);
  dt_show_times(&start, "[dev]", "to load the image.");

  // copy over image now that width and height are sure to be correct:
  const dt_image_t *img = dt_image_cache_read_get(darktable.image_cache, dev->image_storage.id);
  dev->image_storage = *img;
  // but don't lock the real thing, as that would avoid any writers to change stuff.
  // (such as raw loading or star rating changes)
  dt_image_cache_read_release(darktable.image_cache, img);

  // failed to load raw?
  if(!buf.buf) return;

  dt_dev_pixelpipe_set_input(dev->pipe, dev, (float *)buf.buf, buf.width, buf.height, 1.0);

  if(dev->image_loading)
  {
    // init pixel pipeline
    dt_dev_pixelpipe_cleanup_nodes(dev->pipe);
    dt_dev_pixelpipe_create_nodes(dev->pipe, dev);
    if(dev->image_force_reload) dt_dev_pixelpipe_flush_caches(dev->pipe);
    dev->image_dirty = 1;
    dev->image_force_reload = 0;
    if(dev->gui_attached)
    {
      // during load, a mipf update could have been issued.
      dev->preview_input_changed = 1;
      dev->preview_dirty = 1;
      dev->gui_synch = 1; // notify gui thread we want to synch (call gui_update in the modules)
      dev->preview_pipe->changed |= DT_DEV_PIPE_SYNCH;
    }
    dev->pipe->changed |= DT_DEV_PIPE_SYNCH;
  }

  dt_dev_zoom_t zoom;
  float zoom_x, zoom_y, scale;
  int x, y;

  // adjust pipeline according to changed flag set by {add,pop}_history_item.
restart:
  if(dev->gui_leaving)
  {
    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
    dt_control_log_busy_leave();
    return;
  }
  dev->pipe->input_timestamp = dev->timestamp;
  // this locks dev->history_mutex.
  dt_dev_pixelpipe_change(dev->pipe, dev);
  // determine scale according to new dimensions
  DT_CTL_GET_GLOBAL(zoom, dev_zoom);
  DT_CTL_GET_GLOBAL(zoom_x, dev_zoom_x);
  DT_CTL_GET_GLOBAL(zoom_y, dev_zoom_y);

  scale = dt_dev_get_zoom_scale(dev, zoom, 1.0f, 0);
  dev->capwidth  = MIN(MIN(dev->width,  dev->pipe->processed_width *scale), darktable.thumbnail_width);
  dev->capheight = MIN(MIN(dev->height, dev->pipe->processed_height*scale), darktable.thumbnail_height);
  x = MAX(0, scale*dev->pipe->processed_width *(.5+zoom_x)-dev->capwidth/2);
  y = MAX(0, scale*dev->pipe->processed_height*(.5+zoom_y)-dev->capheight/2);

  dt_get_times(&start);
  if(dt_dev_pixelpipe_process(dev->pipe, dev, x, y, dev->capwidth, dev->capheight, scale))
  {
    // interrupted because image changed?
    if(dev->image_force_reload)
    {
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      dt_control_log_busy_leave();
      return;
    }
    // or because the pipeline changed?
    else goto restart;
  }
  dt_show_times(&start, "[dev_process_image] pixel pipeline processing", NULL);

  // maybe we got zoomed/panned in the meantime?
  if(dev->pipe->changed != DT_DEV_PIPE_UNCHANGED) goto restart;

  // cool, we got a new image!
  dev->image_dirty = 0;
  dev->image_loading = 0;

  dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
  dt_control_queue_redraw_center();
  dt_control_log_busy_leave();
}
Пример #9
0
void dt_dev_process_preview_job(dt_develop_t *dev)
{
  dt_mipmap_buffer_t buf;
  if(dev->image_loading)
  {
    // raw is already loading, no use starting another file access, we wait.
    return;
  }

  dt_control_log_busy_enter();
  dev->preview_pipe->input_timestamp = dev->timestamp;
  dev->preview_dirty = 1;

  // lock if there, issue a background load, if not (best-effort for mip f).
  dt_mipmap_cache_read_get(darktable.mipmap_cache, &buf, dev->image_storage.id, DT_MIPMAP_F, 0);
  if(!buf.buf)
  {
    dt_control_log_busy_leave();
    return; // not loaded yet. load will issue a gtk redraw on completion, which in turn will trigger us again later.
  }
  // init pixel pipeline for preview.
  dt_dev_pixelpipe_set_input(dev->preview_pipe, dev, (float *)buf.buf, buf.width, buf.height, dev->image_storage.width/(float)buf.width);

  if(dev->preview_loading)
  {
    dt_dev_pixelpipe_cleanup_nodes(dev->preview_pipe);
    dt_dev_pixelpipe_create_nodes(dev->preview_pipe, dev);
    dt_dev_pixelpipe_flush_caches(dev->preview_pipe);
    dev->preview_loading = 0;
  }

  // if raw loaded, get new mipf
  if(dev->preview_input_changed)
  {
    dt_dev_pixelpipe_flush_caches(dev->preview_pipe);
    dev->preview_input_changed = 0;
  }

  // always process the whole downsampled mipf buffer, to allow for fast scrolling and mip4 write-through.
restart:
  if(dev->gui_leaving)
  {
    dt_control_log_busy_leave();
    dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
    return;
  }
  // adjust pipeline according to changed flag set by {add,pop}_history_item.
  // this locks dev->history_mutex.
  dt_times_t start;
  dt_get_times(&start);
  dt_dev_pixelpipe_change(dev->preview_pipe, dev);
  if(dt_dev_pixelpipe_process(dev->preview_pipe, dev, 0, 0, dev->preview_pipe->processed_width*dev->preview_downsampling, dev->preview_pipe->processed_height*dev->preview_downsampling, dev->preview_downsampling))
  {
    if(dev->preview_loading || dev->preview_input_changed)
    {
      dt_control_log_busy_leave();
      dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
      return;
    }
    else goto restart;
  }
  dt_show_times(&start, "[dev_process_preview] pixel pipeline processing", NULL);

  dev->preview_dirty = 0;
  dt_control_log_busy_leave();
  dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);
}
Пример #10
0
void
dt_view_image_expose(
    dt_view_image_over_t *image_over,
    uint32_t imgid,
    cairo_t *cr,
    int32_t width,
    int32_t height,
    int32_t zoom,
    int32_t px,
    int32_t py)
{
    const double start = dt_get_wtime();
    // some performance tuning stuff, for your pleasure.
    // on my machine with 7 image per row it seems grouping has the largest
    // impact from around 400ms -> 55ms per redraw.
#define DRAW_THUMB 1
#define DRAW_COLORLABELS 1
#define DRAW_GROUPING 1
#define DRAW_SELECTED 1
#define DRAW_HISTORY 1

#if DRAW_THUMB == 1
    // this function is not thread-safe (gui-thread only), so we
    // can safely allocate this leaking bit of memory to decompress thumbnails:
    static int first_time = 1;
    static uint8_t *scratchmem = NULL;
    if(first_time)
    {
        // scratchmem might still be NULL after this, if compression is off.
        scratchmem = dt_mipmap_cache_alloc_scratchmem(darktable.mipmap_cache);
        first_time = 0;
    }
#endif

    cairo_save (cr);
    float bgcol = 0.4, fontcol = 0.425, bordercol = 0.1, outlinecol = 0.2;
    int selected = 0, altered = 0, imgsel = -1, is_grouped = 0;
    // this is a gui thread only thing. no mutex required:
    imgsel = darktable.control->global_settings.lib_image_mouse_over_id;

#if DRAW_SELECTED == 1
    /* clear and reset statements */
    DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.is_selected);
    DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.is_selected);
    /* bind imgid to prepared statments */
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.is_selected, 1, imgid);
    /* lets check if imgid is selected */
    if(sqlite3_step(darktable.view_manager->statements.is_selected) == SQLITE_ROW)
        selected = 1;
#endif

#if DRAW_HISTORY == 1
    DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.have_history);
    DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.have_history);
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.have_history, 1, imgid);

    /* lets check if imgid has history */
    if(sqlite3_step(darktable.view_manager->statements.have_history) == SQLITE_ROW)
        altered = 1;
#endif

    const dt_image_t *img = dt_image_cache_read_testget(darktable.image_cache, imgid);

#if DRAW_GROUPING == 1
    DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.get_grouped);
    DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.get_grouped);
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.get_grouped, 1, imgid);
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.get_grouped, 2, imgid);

    /* lets check if imgid is in a group */
    if(sqlite3_step(darktable.view_manager->statements.get_grouped) == SQLITE_ROW)
        is_grouped = 1;
    else if(img && darktable.gui->expanded_group_id == img->group_id)
        darktable.gui->expanded_group_id = -1;
#endif

    if(selected == 1)
    {
        outlinecol = 0.4;
        bgcol = 0.6;
        fontcol = 0.5;
    }
    if(imgsel == imgid)
    {
        bgcol = 0.8;  // mouse over
        fontcol = 0.7;
        outlinecol = 0.6;
        // if the user points at this image, we really want it:
        if(!img)
            img = dt_image_cache_read_get(darktable.image_cache, imgid);
    }
    float imgwd = 0.90f;
    if(zoom == 1)
    {
        imgwd = .97f;
        // cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);
    }
    else
    {
        double x0 = 1, y0 = 1, rect_width = width-2, rect_height = height-2, radius = 5;
        double x1, y1, off, off1;

        x1=x0+rect_width;
        y1=y0+rect_height;
        off=radius*0.666;
        off1 = radius-off;
        cairo_move_to  (cr, x0, y0 + radius);
        cairo_curve_to (cr, x0, y0+off1, x0+off1 , y0, x0 + radius, y0);
        cairo_line_to (cr, x1 - radius, y0);
        cairo_curve_to (cr, x1-off1, y0, x1, y0+off1, x1, y0 + radius);
        cairo_line_to (cr, x1 , y1 - radius);
        cairo_curve_to (cr, x1, y1-off1, x1-off1, y1, x1 - radius, y1);
        cairo_line_to (cr, x0 + radius, y1);
        cairo_curve_to (cr, x0+off1, y1, x0, y1-off1, x0, y1- radius);
        cairo_close_path (cr);
        cairo_set_source_rgb(cr, bgcol, bgcol, bgcol);
        cairo_fill_preserve(cr);
        cairo_set_line_width(cr, 0.005*width);
        cairo_set_source_rgb(cr, outlinecol, outlinecol, outlinecol);
        cairo_stroke(cr);

        if(img)
        {
            const char *ext = img->filename + strlen(img->filename);
            while(ext > img->filename && *ext != '.') ext--;
            ext++;
            cairo_set_source_rgb(cr, fontcol, fontcol, fontcol);
            cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
            cairo_set_font_size (cr, .25*width);
            cairo_text_extents_t text_extends;
            cairo_text_extents (cr, ext, &text_extends);
            cairo_move_to (cr, .025*width - text_extends.x_bearing, .24*height);
            cairo_show_text (cr, ext);
        }
    }

    dt_mipmap_buffer_t buf;
    dt_mipmap_size_t mip =
        dt_mipmap_cache_get_matching_size(
            darktable.mipmap_cache,
            imgwd*width, imgwd*height);
    dt_mipmap_cache_read_get(
        darktable.mipmap_cache,
        &buf,
        imgid,
        mip,
        0);
#if DRAW_THUMB == 1
    float scale = 1.0;
    // decompress image, if necessary. if compression is off, scratchmem will be == NULL,
    // so get the real pointer back:
    uint8_t *buf_decompressed = dt_mipmap_cache_decompress(&buf, scratchmem);

    cairo_surface_t *surface = NULL;
    if(buf.buf)
    {
        const int32_t stride = cairo_format_stride_for_width (CAIRO_FORMAT_RGB24, buf.width);
        surface = cairo_image_surface_create_for_data (buf_decompressed, CAIRO_FORMAT_RGB24, buf.width, buf.height, stride);
        if(zoom == 1)
        {
            scale = fminf(
                        fminf(darktable.thumbnail_width, width) / (float)buf.width,
                        fminf(darktable.thumbnail_height, height) / (float)buf.height
                    );
        }
        else scale = fminf(width*imgwd/(float)buf.width, height*imgwd/(float)buf.height);
    }

    // draw centered and fitted:
    cairo_save(cr);
    cairo_translate(cr, width/2.0, height/2.0f);
    cairo_scale(cr, scale, scale);

    if(buf.buf)
    {
        cairo_translate(cr, -.5f*buf.width, -.5f*buf.height);
        cairo_set_source_surface (cr, surface, 0, 0);
        // set filter no nearest:
        // in skull mode, we want to see big pixels.
        // in 1 iir mode for the right mip, we want to see exactly what the pipe gave us, 1:1 pixel for pixel.
        // in between, filtering just makes stuff go unsharp.
        if((buf.width <= 8 && buf.height <= 8) || fabsf(scale - 1.0f) < 0.01f)
            cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
        cairo_rectangle(cr, 0, 0, buf.width, buf.height);
        cairo_fill(cr);
        cairo_surface_destroy (surface);

        cairo_rectangle(cr, 0, 0, buf.width, buf.height);
    }

    // border around image
    const float border = zoom == 1 ? 16/scale : 2/scale;
    cairo_set_source_rgb(cr, bordercol, bordercol, bordercol);
    if(buf.buf && selected)
    {
        cairo_set_line_width(cr, 1./scale);
        if(zoom == 1)
        {
            // draw shadow around border
            cairo_set_source_rgb(cr, 0.2, 0.2, 0.2);
            cairo_stroke(cr);
            // cairo_new_path(cr);
            cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
            float alpha = 1.0f;
            for(int k=0; k<16; k++)
            {
                cairo_rectangle(cr, 0, 0, buf.width, buf.height);
                cairo_new_sub_path(cr);
                cairo_rectangle(cr, -k/scale, -k/scale, buf.width+2.*k/scale, buf.height+2.*k/scale);
                cairo_set_source_rgba(cr, 0, 0, 0, alpha);
                alpha *= 0.6f;
                cairo_fill(cr);
            }
        }
        else
        {
            cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
            cairo_new_sub_path(cr);
            cairo_rectangle(cr, -border, -border, buf.width+2.*border, buf.height+2.*border);
            cairo_stroke_preserve(cr);
            cairo_set_source_rgb(cr, 1.0-bordercol, 1.0-bordercol, 1.0-bordercol);
            cairo_fill(cr);
        }
    }
    else if(buf.buf)
    {
        cairo_set_line_width(cr, 1);
        cairo_stroke(cr);
    }
    cairo_restore(cr);
#endif
    if(buf.buf)
        dt_mipmap_cache_read_release(darktable.mipmap_cache, &buf);

    const float fscale = fminf(width, height);
    if(imgsel == imgid)
    {
        // draw mouseover hover effects, set event hook for mouse button down!
        *image_over = DT_VIEW_DESERT;
        cairo_set_line_width(cr, 1.5);
        cairo_set_source_rgb(cr, outlinecol, outlinecol, outlinecol);
        cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
        float r1, r2;
        if(zoom != 1)
        {
            r1 = 0.05*width;
            r2 = 0.022*width;
        }
        else
        {
            r1 = 0.015*fscale;
            r2 = 0.007*fscale;
        }

        float x, y;
        if(zoom != 1) y = 0.90*height;
        else y = .12*fscale;
        gboolean image_is_rejected = (img && ((img->flags & 0x7) == 6));

        if(img) for(int k=0; k<5; k++)
            {
                if(zoom != 1) x = (0.41+k*0.12)*width;
                else x = (.08+k*0.04)*fscale;

                if(!image_is_rejected) //if rejected: draw no stars
                {
                    dt_view_star(cr, x, y, r1, r2);
                    if((px - x)*(px - x) + (py - y)*(py - y) < r1*r1)
                    {
                        *image_over = DT_VIEW_STAR_1 + k;
                        cairo_fill(cr);
                    }
                    else if((img->flags & 0x7) > k)
                    {
                        cairo_fill_preserve(cr);
                        cairo_set_source_rgb(cr, 1.0-bordercol, 1.0-bordercol, 1.0-bordercol);
                        cairo_stroke(cr);
                        cairo_set_source_rgb(cr, outlinecol, outlinecol, outlinecol);
                    }
                    else cairo_stroke(cr);
                }
            }

        //Image rejected?
        if(zoom !=1) x = 0.11*width;
        else x = .04*fscale;

        if (image_is_rejected)
            cairo_set_source_rgb(cr, 1., 0., 0.);

        if((px - x)*(px - x) + (py - y)*(py - y) < r1*r1)
        {
            *image_over = DT_VIEW_REJECT; //mouse sensitive
            cairo_new_sub_path(cr);
            cairo_arc(cr, x, y, (r1+r2)*.5, 0, 2.0f*M_PI);
            cairo_stroke(cr);
        }

        if (image_is_rejected)
            cairo_set_line_width(cr, 2.5);

        //reject cross:
        cairo_move_to(cr, x-r2, y-r2);
        cairo_line_to(cr, x+r2, y+r2);
        cairo_move_to(cr, x+r2, y-r2);
        cairo_line_to(cr, x-r2, y+r2);
        cairo_close_path(cr);
        cairo_stroke(cr);
        cairo_set_source_rgb(cr, outlinecol, outlinecol, outlinecol);
        cairo_set_line_width(cr, 1.5);


        // image part of a group?
        if(is_grouped && darktable.gui && darktable.gui->grouping)
        {
            // draw grouping icon and border if the current group is expanded
            // align to the right, left of altered
            float s = (r1+r2)*.75;
            float _x, _y;
            if(zoom != 1)
            {
                _x = width*0.9 - s*2.5;
                _y = height*0.1 - s*.4;
            }
            else
            {
                _x = (.04+7*0.04-1.1*.04)*fscale;
                _y = y - (.17*.04)*fscale;
            }
            cairo_save(cr);
            if(img && (imgid != img->group_id))
                cairo_set_source_rgb(cr, fontcol, fontcol, fontcol);
            dtgtk_cairo_paint_grouping(cr, _x, _y, s, s, 23);
            cairo_restore(cr);
            // mouse is over the grouping icon
            if(img && abs(px-_x-.5*s) <= .8*s && abs(py-_y-.5*s) <= .8*s)
                *image_over = DT_VIEW_GROUP;
        }

        // image altered?
        if(altered)
        {
            // align to right
            float s = (r1+r2)*.5;
            if(zoom != 1)
            {
                x = width*0.9;
                y = height*0.1;
            }
            else x = (.04+7*0.04)*fscale;
            dt_view_draw_altered(cr, x, y, s);
            //g_print("px = %d, x = %.4f, py = %d, y = %.4f\n", px, x, py, y);
            if(img && abs(px-x) <= 1.2*s && abs(py-y) <= 1.2*s) // mouse hovers over the altered-icon -> history tooltip!
            {
                darktable.gui->center_tooltip = 1;
            }
        }
    }

    // kill all paths, in case img was not loaded yet, or is blocked:
    cairo_new_path(cr);

#if DRAW_COLORLABELS == 1
    // TODO: make mouse sensitive, just as stars!
    // TODO: cache in image struct!
    {
        // color labels:
        const float x = zoom == 1 ? (0.07)*fscale : .21*width;
        const float y = zoom == 1 ? 0.17*fscale: 0.1*height;
        const float r = zoom == 1 ? 0.01*fscale : 0.03*width;

        /* clear and reset prepared statement */
        DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.get_color);
        DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.get_color);

        /* setup statement and iterate rows */
        DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.get_color, 1, imgid);
        while(sqlite3_step(darktable.view_manager->statements.get_color) == SQLITE_ROW)
        {
            cairo_save(cr);
            int col = sqlite3_column_int(darktable.view_manager->statements.get_color, 0);
            // see src/dtgtk/paint.c
            dtgtk_cairo_paint_label(cr, x+(3*r*col)-5*r, y-r, r*2, r*2, col);
            cairo_restore(cr);
        }
    }
#endif

    if(img && (zoom == 1))
    {
        // some exif data
        cairo_set_source_rgb(cr, .7, .7, .7);
        cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
        cairo_set_font_size (cr, .025*fscale);

        cairo_move_to (cr, .02*fscale, .04*fscale);
        // cairo_show_text(cr, img->filename);
        cairo_text_path(cr, img->filename);
        char exifline[50];
        cairo_move_to (cr, .02*fscale, .08*fscale);
        dt_image_print_exif(img, exifline, 50);
        cairo_text_path(cr, exifline);
        cairo_fill_preserve(cr);
        cairo_set_line_width(cr, 1.0);
        cairo_set_source_rgb(cr, 0.3, 0.3, 0.3);
        cairo_stroke(cr);
    }

    if(img) dt_image_cache_read_release(darktable.image_cache, img);
    cairo_restore(cr);
    // if(zoom == 1) cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);

    const double end = dt_get_wtime();
    dt_print(DT_DEBUG_PERF, "[lighttable] image expose took %0.04f sec\n", end-start);
}