1 /*
2     This file is part of darktable,
3     Copyright (C) 2010-2021 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 #include "common/debug.h"
19 #include "common/darktable.h"
20 #include "bauhaus/bauhaus.h"
21 #include "common/variables.h"
22 #include "common/colorlabels.h"
23 #include "common/darktable.h"
24 #include "common/file_location.h"
25 #include "common/image.h"
26 #include "common/image_cache.h"
27 #include "common/metadata.h"
28 #include "common/opencl.h"
29 #include "common/utility.h"
30 #include "common/tags.h"
31 #include "control/conf.h"
32 
33 #include <stdio.h>
34 #include <string.h>
35 #include <time.h>
36 
37 typedef struct dt_variables_data_t
38 {
39   /** cached values that shouldn't change between variables in the same expansion process */
40   struct tm time;
41   time_t exif_time;
42   guint sequence;
43 
44   /* export settings for image maximum width and height taken from GUI */
45   int max_width;
46   int max_height;
47 
48   char *homedir;
49   char *pictures_folder;
50   const char *file_ext;
51 
52   gboolean have_exif_tm;
53   int exif_iso;
54   char *camera_maker;
55   char *camera_alias;
56   char *exif_lens;
57   int version;
58   int stars;
59   struct tm exif_tm;
60 
61   float exif_exposure;
62   float exif_exposure_bias;
63   float exif_aperture;
64   float exif_focal_length;
65   float exif_focus_distance;
66   double longitude;
67   double latitude;
68   double elevation;
69 
70   uint32_t tags_flags;
71 
72   int flags;
73 
74 } dt_variables_data_t;
75 
76 static char *expand(dt_variables_params_t *params, char **source, char extra_stop);
77 
78 // gather some data that might be used for variable expansion
init_expansion(dt_variables_params_t * params,gboolean iterate)79 static void init_expansion(dt_variables_params_t *params, gboolean iterate)
80 {
81   if(iterate) params->data->sequence++;
82 
83   params->data->homedir = dt_loc_get_home_dir(NULL);
84 
85   if(g_get_user_special_dir(G_USER_DIRECTORY_PICTURES) == NULL)
86     params->data->pictures_folder = g_build_path(G_DIR_SEPARATOR_S, params->data->homedir, "Pictures", (char *)NULL);
87   else
88     params->data->pictures_folder = g_strdup(g_get_user_special_dir(G_USER_DIRECTORY_PICTURES));
89 
90   if(params->filename)
91   {
92     params->data->file_ext = (g_strrstr(params->filename, ".") + 1);
93     if(params->data->file_ext == (gchar *)1) params->data->file_ext = params->filename + strlen(params->filename);
94   }
95   else
96     params->data->file_ext = NULL;
97 
98   /* image exif time */
99   params->data->have_exif_tm = FALSE;
100   params->data->exif_iso = 100;
101   params->data->camera_maker = NULL;
102   params->data->camera_alias = NULL;
103   params->data->exif_lens = NULL;
104   params->data->version = 0;
105   params->data->stars = 0;
106   params->data->exif_exposure = 0.0f;
107   params->data->exif_exposure_bias = NAN;
108   params->data->exif_aperture = 0.0f;
109   params->data->exif_focal_length = 0.0f;
110   params->data->exif_focus_distance = 0.0f;
111   params->data->longitude = 0.0f;
112   params->data->latitude = 0.0f;
113   params->data->elevation = 0.0f;
114   if(params->imgid)
115   {
116     const dt_image_t *img = params->img ? (dt_image_t *)params->img
117                                         : dt_image_cache_get(darktable.image_cache, params->imgid, 'r');
118     if(sscanf(img->exif_datetime_taken, "%d:%d:%d %d:%d:%d", &params->data->exif_tm.tm_year, &params->data->exif_tm.tm_mon,
119       &params->data->exif_tm.tm_mday, &params->data->exif_tm.tm_hour, &params->data->exif_tm.tm_min, &params->data->exif_tm.tm_sec) == 6)
120     {
121       params->data->exif_tm.tm_year -= 1900;
122       params->data->exif_tm.tm_mon--;
123       params->data->have_exif_tm = TRUE;
124     }
125     params->data->exif_iso = img->exif_iso;
126     params->data->camera_maker = g_strdup(img->camera_maker);
127     params->data->camera_alias = g_strdup(img->camera_alias);
128     params->data->exif_lens = g_strdup(img->exif_lens);
129     params->data->version = img->version;
130     params->data->stars = (img->flags & 0x7);
131     if(params->data->stars == 6) params->data->stars = -1;
132 
133     params->data->exif_exposure = img->exif_exposure;
134     params->data->exif_exposure_bias = img->exif_exposure_bias;
135     params->data->exif_aperture = img->exif_aperture;
136     params->data->exif_focal_length = img->exif_focal_length;
137     if(!isnan(img->exif_focus_distance) && fpclassify(img->exif_focus_distance) != FP_ZERO)
138       params->data->exif_focus_distance = img->exif_focus_distance;
139     if(!isnan(img->geoloc.longitude)) params->data->longitude = img->geoloc.longitude;
140     if(!isnan(img->geoloc.latitude)) params->data->latitude = img->geoloc.latitude;
141     if(!isnan(img->geoloc.elevation)) params->data->elevation = img->geoloc.elevation;
142 
143     params->data->flags = img->flags;
144 
145     if(params->img == NULL) dt_image_cache_read_release(darktable.image_cache, img);
146   }
147   else if (params->data->exif_time) {
148     localtime_r(&params->data->exif_time, &params->data->exif_tm);
149     params->data->have_exif_tm = TRUE;
150   }
151 }
152 
cleanup_expansion(dt_variables_params_t * params)153 static void cleanup_expansion(dt_variables_params_t *params)
154 {
155   g_free(params->data->homedir);
156   g_free(params->data->pictures_folder);
157   g_free(params->data->camera_maker);
158   g_free(params->data->camera_alias);
159 }
160 
has_prefix(char ** str,const char * prefix)161 static inline gboolean has_prefix(char **str, const char *prefix)
162 {
163   gboolean res = g_str_has_prefix(*str, prefix);
164   if(res) *str += strlen(prefix);
165   return res;
166 }
167 
get_base_value(dt_variables_params_t * params,char ** variable)168 static char *get_base_value(dt_variables_params_t *params, char **variable)
169 {
170   char *result = NULL;
171   gboolean escape = TRUE;
172 
173   struct tm exif_tm = params->data->have_exif_tm ? params->data->exif_tm : params->data->time;
174 
175   if(has_prefix(variable, "YEAR"))
176     result = g_strdup_printf("%.4d", params->data->time.tm_year + 1900);
177   else if(has_prefix(variable, "MONTH"))
178     result = g_strdup_printf("%.2d", params->data->time.tm_mon + 1);
179   else if(has_prefix(variable, "DAY"))
180     result = g_strdup_printf("%.2d", params->data->time.tm_mday);
181   else if(has_prefix(variable, "HOUR"))
182     result = g_strdup_printf("%.2d", params->data->time.tm_hour);
183   else if(has_prefix(variable, "MINUTE"))
184     result = g_strdup_printf("%.2d", params->data->time.tm_min);
185   else if(has_prefix(variable, "SECOND"))
186     result = g_strdup_printf("%.2d", params->data->time.tm_sec);
187 
188   else if(has_prefix(variable, "EXIF_YEAR"))
189     result = g_strdup_printf("%.4d", exif_tm.tm_year + 1900);
190   else if(has_prefix(variable, "EXIF_MONTH"))
191     result = g_strdup_printf("%.2d", exif_tm.tm_mon + 1);
192   else if(has_prefix(variable, "EXIF_DAY"))
193     result = g_strdup_printf("%.2d", exif_tm.tm_mday);
194   else if(has_prefix(variable, "EXIF_HOUR"))
195     result = g_strdup_printf("%.2d", exif_tm.tm_hour);
196   else if(has_prefix(variable, "EXIF_MINUTE"))
197     result = g_strdup_printf("%.2d", exif_tm.tm_min);
198   else if(has_prefix(variable, "EXIF_SECOND"))
199     result = g_strdup_printf("%.2d", exif_tm.tm_sec);
200   else if(has_prefix(variable, "EXIF_ISO"))
201     result = g_strdup_printf("%d", params->data->exif_iso);
202   else if(has_prefix(variable, "NL") && g_strcmp0(params->jobcode, "infos") == 0)
203     result = g_strdup_printf("\n");
204   else if(has_prefix(variable, "EXIF_EXPOSURE_BIAS"))
205   {
206     if(!isnan(params->data->exif_exposure_bias))
207       result = g_strdup_printf("%+.2f", params->data->exif_exposure_bias);
208   }
209   else if(has_prefix(variable, "EXIF_EXPOSURE"))
210   {
211     result = dt_util_format_exposure(params->data->exif_exposure);
212     // for job other than info (export) we strip the slash char
213     if(g_strcmp0(params->jobcode, "infos") != 0)
214     {
215       gchar *res = dt_util_str_replace(result, "/", "_");
216       g_free(result);
217       result = res;
218     }
219   }
220   else if(has_prefix(variable, "EXIF_APERTURE"))
221     result = g_strdup_printf("%.1f", params->data->exif_aperture);
222   else if(has_prefix(variable, "EXIF_FOCAL_LENGTH"))
223     result = g_strdup_printf("%d", (int)params->data->exif_focal_length);
224   else if(has_prefix(variable, "EXIF_FOCUS_DISTANCE"))
225     result = g_strdup_printf("%.2f", params->data->exif_focus_distance);
226   else if(has_prefix(variable, "LONGITUDE"))
227   {
228     if(dt_conf_get_bool("plugins/lighttable/metadata_view/pretty_location")
229        && g_strcmp0(params->jobcode, "infos") == 0)
230     {
231       result = dt_util_longitude_str(params->data->longitude);
232     }
233     else
234     {
235       gchar NS = params->data->longitude < 0 ? 'W' : 'E';
236       result = g_strdup_printf("%c%010.6f", NS, fabs(params->data->longitude));
237     }
238   }
239   else if(has_prefix(variable, "LATITUDE"))
240   {
241     if(dt_conf_get_bool("plugins/lighttable/metadata_view/pretty_location")
242        && g_strcmp0(params->jobcode, "infos") == 0)
243     {
244       result = dt_util_latitude_str(params->data->latitude);
245     }
246     else
247     {
248       gchar NS = params->data->latitude < 0 ? 'S' : 'N';
249       result = g_strdup_printf("%c%09.6f", NS, fabs(params->data->latitude));
250     }
251   }
252   else if(has_prefix(variable, "ELEVATION"))
253     result = g_strdup_printf("%.2f", params->data->elevation);
254   else if(has_prefix(variable, "MAKER"))
255     result = g_strdup(params->data->camera_maker);
256   else if(has_prefix(variable, "MODEL"))
257     result = g_strdup(params->data->camera_alias);
258   else if(has_prefix(variable, "LENS"))
259     result = g_strdup(params->data->exif_lens);
260   else if(has_prefix(variable, "ID"))
261     result = g_strdup_printf("%d", params->imgid);
262   else if(has_prefix(variable, "VERSION_NAME"))
263   {
264     GList *res = dt_metadata_get(params->imgid, "Xmp.darktable.version_name", NULL);
265     if(res != NULL)
266     {
267       result = g_strdup((char *)res->data);
268     }
269     g_list_free_full(res, &g_free);
270   }
271   else if(has_prefix(variable, "VERSION_IF_MULTI"))
272   {
273     sqlite3_stmt *stmt;
274 
275     // count duplicates
276     DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
277                                 "SELECT COUNT(1)"
278                                 " FROM images AS i1"
279                                 " WHERE EXISTS (SELECT 'y' FROM images AS i2"
280                                 "               WHERE  i2.id = ?1"
281                                 "               AND    i1.film_id = i2.film_id"
282                                 "               AND    i1.filename = i2.filename)",
283                                 -1, &stmt, NULL);
284     DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, params->imgid);
285 
286     if(sqlite3_step(stmt) == SQLITE_ROW)
287     {
288       const int count = sqlite3_column_int(stmt, 0);
289       //only return data if more than one matching image
290       if(count > 1)
291         result = g_strdup_printf("%d", params->data->version);
292     }
293     sqlite3_finalize (stmt);
294   }
295   else if(has_prefix(variable, "VERSION"))
296     result = g_strdup_printf("%d", params->data->version);
297   else if(has_prefix(variable, "JOBCODE"))
298     result = g_strdup(params->jobcode);
299   else if(has_prefix(variable, "ROLL_NAME"))
300   {
301     if(params->filename)
302     {
303       gchar *dirname = g_path_get_dirname(params->filename);
304       result = g_path_get_basename(dirname);
305       g_free(dirname);
306     }
307   }
308   else if(has_prefix(variable, "FILE_DIRECTORY"))
309   {
310     // undocumented : backward compatibility
311     if(params->filename)
312       result = g_path_get_dirname(params->filename);
313   }
314   else if(has_prefix(variable, "FILE_FOLDER"))
315   {
316     if(params->filename)
317       result = g_path_get_dirname(params->filename);
318   }
319   else if(has_prefix(variable, "FILE_NAME"))
320   {
321     if(params->filename)
322     {
323       result = g_path_get_basename(params->filename);
324       char *dot = g_strrstr(result, ".");
325       if(dot) *dot = '\0';
326     }
327   }
328   else if(has_prefix(variable, "FILE_EXTENSION"))
329     result = g_strdup(params->data->file_ext);
330   else if(has_prefix(variable, "SEQUENCE"))
331   {
332     uint8_t nb_digit = 4;
333     if(g_ascii_isdigit(*variable[0]))
334     {
335       nb_digit = (uint8_t)*variable[0] & 0b1111;
336       (*variable) ++;
337     }
338     result = g_strdup_printf("%.*d", nb_digit, params->sequence >= 0 ? params->sequence : params->data->sequence);
339   }
340   else if(has_prefix(variable, "USERNAME"))
341     result = g_strdup(g_get_user_name());
342   else if(has_prefix(variable, "HOME_FOLDER"))
343     result = g_strdup(params->data->homedir); // undocumented : backward compatibility
344   else if(has_prefix(variable, "HOME"))
345     result = g_strdup(params->data->homedir);
346   else if(has_prefix(variable, "PICTURES_FOLDER"))
347     result = g_strdup(params->data->pictures_folder);
348   else if(has_prefix(variable, "DESKTOP_FOLDER"))
349     result = g_strdup(g_get_user_special_dir(G_USER_DIRECTORY_DESKTOP)); // undocumented : backward compatibility
350   else if(has_prefix(variable, "DESKTOP"))
351     result = g_strdup(g_get_user_special_dir(G_USER_DIRECTORY_DESKTOP));
352   else if(has_prefix(variable, "STARS"))
353     result = g_strdup_printf("%d", params->data->stars);
354   else if(has_prefix(variable, "RATING_ICONS"))
355   {
356     switch(params->data->stars)
357     {
358       case -1:
359         result = g_strdup("X");
360         break;
361       case 1:
362         result = g_strdup("★");
363         break;
364       case 2:
365         result = g_strdup("★★");
366         break;
367       case 3:
368         result = g_strdup("★★★");
369         break;
370       case 4:
371         result = g_strdup("★★★★");
372         break;
373       case 5:
374         result = g_strdup("★★★★★");
375         break;
376       default:
377         result = g_strdup("");
378         break;
379     }
380   }
381   else if((has_prefix(variable, "LABELS_ICONS") ||
382            has_prefix(variable, "LABELS_COLORICONS"))
383           && g_strcmp0(params->jobcode, "infos") == 0)
384   {
385     escape = FALSE;
386     GList *res = dt_metadata_get(params->imgid, "Xmp.darktable.colorlabels", NULL);
387     for(GList *res_iter = res; res_iter; res_iter = g_list_next(res_iter))
388     {
389       const int dot_index = GPOINTER_TO_INT(res_iter->data);
390       const GdkRGBA c = darktable.bauhaus->colorlabels[dot_index];
391       result = dt_util_dstrcat(result,
392                                "<span foreground='#%02x%02x%02x'>⬤ </span>",
393                                (guint)(c.red*255), (guint)(c.green*255), (guint)(c.blue*255));
394     }
395     g_list_free(res);
396   }
397   else if(has_prefix(variable, "LABELS"))
398   {
399     // TODO: currently we concatenate all the color labels with a ',' as a separator. Maybe it's better to
400     // only use the first/last label?
401     GList *res = dt_metadata_get(params->imgid, "Xmp.darktable.colorlabels", NULL);
402     if(res != NULL)
403     {
404       GList *labels = NULL;
405       for(GList *res_iter = res; res_iter; res_iter = g_list_next(res_iter))
406       {
407         labels = g_list_prepend(labels, (char *)(_(dt_colorlabels_to_string(GPOINTER_TO_INT(res_iter->data)))));
408       }
409       labels = g_list_reverse(labels);  // list was built in reverse order, so un-reverse it
410       result = dt_util_glist_to_str(",", labels);
411       g_list_free(labels);
412     }
413     g_list_free(res);
414   }
415   else if(has_prefix(variable, "TITLE"))
416   {
417     GList *res = dt_metadata_get(params->imgid, "Xmp.dc.title", NULL);
418     if(res != NULL)
419     {
420       result = g_strdup((char *)res->data);
421     }
422     g_list_free_full(res, &g_free);
423   }
424   else if(has_prefix(variable, "DESCRIPTION"))
425   {
426     GList *res = dt_metadata_get(params->imgid, "Xmp.dc.description", NULL);
427     if(res != NULL)
428     {
429       result = g_strdup((char *)res->data);
430     }
431     g_list_free_full(res, &g_free);
432   }
433   else if(has_prefix(variable, "CREATOR"))
434   {
435     GList *res = dt_metadata_get(params->imgid, "Xmp.dc.creator", NULL);
436     if(res != NULL)
437     {
438       result = g_strdup((char *)res->data);
439     }
440     g_list_free_full(res, &g_free);
441   }
442   else if(has_prefix(variable, "PUBLISHER"))
443   {
444     GList *res = dt_metadata_get(params->imgid, "Xmp.dc.publisher", NULL);
445     if(res != NULL)
446     {
447       result = g_strdup((char *)res->data);
448     }
449     g_list_free_full(res, &g_free);
450   }
451   else if(has_prefix(variable, "RIGHTS"))
452   {
453     GList *res = dt_metadata_get(params->imgid, "Xmp.dc.rights", NULL);
454     if(res != NULL)
455     {
456       result = g_strdup((char *)res->data);
457     }
458     g_list_free_full(res, &g_free);
459   }
460   else if(has_prefix(variable, "OPENCL_ACTIVATED"))
461   {
462     if(dt_opencl_is_enabled())
463       result = g_strdup(_("yes"));
464     else
465       result = g_strdup(_("no"));
466   }
467   else if(has_prefix(variable, "MAX_WIDTH"))
468     result = g_strdup_printf("%d", params->data->max_width);
469   else if(has_prefix(variable, "MAX_HEIGHT"))
470     result = g_strdup_printf("%d", params->data->max_height);
471   else if (has_prefix(variable, "CATEGORY"))
472   {
473     // CATEGORY should be followed by n [0,9] and "(category)". category can contain 0 or more '|'
474     if (g_ascii_isdigit(*variable[0]))
475     {
476       const uint8_t level = (uint8_t)*variable[0] & 0b1111;
477       (*variable) ++;
478       if (*variable[0] == '(')
479       {
480         char *category = g_strdup(*variable + 1);
481         char *end = g_strstr_len(category, -1, ")");
482         if (end)
483         {
484           end[0] = '|';
485           end[1] = '\0';
486           (*variable) += strlen(category) + 1;
487           char *tag = dt_tag_get_subtags(params->imgid, category, (int)level);
488           if (tag)
489           {
490             result = g_strdup(tag);
491             g_free(tag);
492           }
493         }
494         g_free(category);
495       }
496     }
497   }
498   else if (has_prefix(variable, "TAGS"))
499   {
500     GList *tags_list = dt_tag_get_list_export(params->imgid, params->data->tags_flags);
501     char *tags = dt_util_glist_to_str(", ", tags_list);
502     g_list_free_full(tags_list, g_free);
503     result = g_strdup(tags);
504     g_free(tags);
505   }
506   else if(has_prefix(variable, "SIDECAR_TXT") && g_strcmp0(params->jobcode, "infos") == 0
507           && (params->data->flags & DT_IMAGE_HAS_TXT))
508   {
509     char *path = dt_image_get_text_path(params->imgid);
510     if(path)
511     {
512       gchar *txt = NULL;
513       if(g_file_get_contents(path, &txt, NULL, NULL))
514       {
515         result = g_strdup_printf("\n%s", txt);
516       }
517       g_free(txt);
518       g_free(path);
519     }
520   }
521   else
522   {
523     // go past what looks like an invalid variable. we only expect to see [a-zA-Z]* in a variable name.
524     while(g_ascii_isalpha(**variable)) (*variable)++;
525   }
526   if(!result) result = g_strdup("");
527 
528   if(params->escape_markup && escape)
529   {
530     gchar *e_res = g_markup_escape_text(result, -1);
531     g_free(result);
532     return e_res;
533   }
534   return result;
535 }
536 
537 // bash style variable manipulation. all patterns are just simple string comparisons!
538 // See here for bash examples and documentation:
539 // http://www.tldp.org/LDP/abs/html/parameter-substitution.html
540 // https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
541 // the descriptions in the comments are referring to the bash behaviour, dt doesn't do it 100% like that!
variable_get_value(dt_variables_params_t * params,char ** variable)542 static char *variable_get_value(dt_variables_params_t *params, char **variable)
543 {
544   // invariant: the variable starts with "$(" which we can skip
545   (*variable) += 2;
546 
547   // first get the value of the variable
548   char *base_value = get_base_value(params, variable); // this is never going to be NULL!
549   const size_t base_value_length = strlen(base_value);
550 
551   // ... and now see if we have to change it
552   const char operation = **variable;
553   if(operation != '\0' && operation != ')') (*variable)++;
554   switch(operation)
555   {
556     case '-':
557       /*
558         $(parameter-default)
559           If parameter not set, use default.
560       */
561       {
562         char *replacement = expand(params, variable, ')');
563         if(*base_value == '\0')
564         {
565           g_free(base_value);
566           base_value = replacement;
567         }
568         else
569           g_free(replacement);
570       }
571       break;
572     case '+':
573       /*
574         $(parameter+alt_value)
575           If parameter set, use alt_value, else use null string.
576       */
577       {
578         char *replacement = expand(params, variable, ')');
579         if(*base_value != '\0')
580         {
581           g_free(base_value);
582           base_value = replacement;
583         }
584         else
585           g_free(replacement);
586       }
587       break;
588     case ':':
589       /*
590         $(var:offset)
591           Variable var expanded, starting from offset.
592 
593         $(var:offset:length)
594           Expansion to a max of length characters of variable var, from offset.
595 
596         If offset evaluates to a number less than zero, the value is used as an offset in characters from the
597         end of the value of parameter. If length evaluates to a number less than zero, it is interpreted as an
598         offset in characters from the end of the value of parameter rather than a number of characters, and the
599         expansion is the characters between offset and that result.
600       */
601       {
602         const glong base_value_utf8_length = g_utf8_strlen(base_value, -1);
603         const glong offset = strtol(*variable, variable, 10);
604 
605         // find where to start
606         char *start; // from where to copy ...
607         if(offset >= 0)
608           start = g_utf8_offset_to_pointer(base_value, MIN(offset, base_value_utf8_length));
609         else
610           start = g_utf8_offset_to_pointer(base_value + base_value_length, MAX(offset, -base_value_utf8_length));
611 
612         // now find the end if there is a length provided
613         char *end = base_value + base_value_length; // ... and until where
614         if(start && **variable == ':')
615         {
616           (*variable)++;
617           const size_t start_utf8_length = g_utf8_strlen(start, -1);
618           const int length = strtol(*variable, variable, 10);
619           if(length >= 0)
620             end = g_utf8_offset_to_pointer(start, MIN(length, start_utf8_length));
621           else
622             end = g_utf8_offset_to_pointer(base_value + base_value_length, MAX(length, -start_utf8_length));
623         }
624 
625         char *_base_value = g_strndup(start, end - start);
626         g_free(base_value);
627         base_value = _base_value;
628       }
629       break;
630     case '#':
631       /*
632         $(var#Pattern)
633           Remove from $var the shortest part of $Pattern that matches the front end of $var.
634       */
635       {
636         char *pattern = expand(params, variable, ')');
637         const size_t pattern_length = strlen(pattern);
638         if(!strncmp(base_value, pattern, pattern_length))
639         {
640           char *_base_value = g_strdup(base_value + pattern_length);
641           g_free(base_value);
642           base_value = _base_value;
643         }
644         g_free(pattern);
645       }
646       break;
647     case '%':
648       /*
649         $(var%Pattern)
650           Remove from $var the shortest part of $Pattern that matches the back end of $var.
651       */
652       {
653         char *pattern = expand(params, variable, ')');
654         const size_t pattern_length = strlen(pattern);
655         if(!strncmp(base_value + base_value_length - pattern_length, pattern, pattern_length))
656           base_value[base_value_length - pattern_length] = '\0';
657         g_free(pattern);
658       }
659       break;
660     case '/':
661       /*
662         replacement. the following cases are possible:
663 
664         $(var/Pattern/Replacement)
665           First match of Pattern, within var replaced with Replacement.
666           If Replacement is omitted, then the first match of Pattern is replaced by nothing, that is, deleted.
667 
668         $(var//Pattern/Replacement)
669           Global replacement. All matches of Pattern, within var replaced with Replacement.
670           As above, if Replacement is omitted, then all occurrences of Pattern are replaced by nothing, that is, deleted.
671 
672         $(var/#Pattern/Replacement)
673           If prefix of var matches Pattern, then substitute Replacement for Pattern.
674 
675         $(var/%Pattern/Replacement)
676           If suffix of var matches Pattern, then substitute Replacement for Pattern.
677       */
678       {
679         const char mode = **variable;
680 
681         if(mode == '/' || mode == '#' || mode == '%') (*variable)++;
682         char *pattern = expand(params, variable, '/');
683         const size_t pattern_length = strlen(pattern);
684         (*variable)++;
685         char *replacement = expand(params, variable, ')');
686         const size_t replacement_length = strlen(replacement);
687 
688         switch(mode)
689         {
690           case '/':
691           {
692             // TODO: write a dt_util_str_replace that can deal with pattern_length ^^
693             char *p = g_strndup(pattern, pattern_length);
694             char *_base_value = dt_util_str_replace(base_value, p, replacement);
695             g_free(p);
696             g_free(base_value);
697             base_value = _base_value;
698             break;
699           }
700           case '#':
701           {
702             if(!strncmp(base_value, pattern, pattern_length))
703             {
704               char *_base_value = g_malloc(base_value_length - pattern_length + replacement_length + 1);
705               char *end = g_stpcpy(_base_value, replacement);
706               g_stpcpy(end, base_value + pattern_length);
707               g_free(base_value);
708               base_value = _base_value;
709             }
710             break;
711           }
712           case '%':
713           {
714             if(!strncmp(base_value + base_value_length - pattern_length, pattern, pattern_length))
715             {
716               char *_base_value = g_malloc(base_value_length - pattern_length + replacement_length + 1);
717               base_value[base_value_length - pattern_length] = '\0';
718               char *end = g_stpcpy(_base_value, base_value);
719               g_stpcpy(end, replacement);
720               g_free(base_value);
721               base_value = _base_value;
722             }
723             break;
724           }
725           default:
726           {
727             // TODO: is there a strstr_len that limits the length of pattern?
728             char *p = g_strndup(pattern, pattern_length);
729             gchar *found = g_strstr_len(base_value, -1, p);
730             g_free(p);
731             if(found)
732             {
733               *found = '\0';
734               char *_base_value = g_malloc(base_value_length - pattern_length + replacement_length + 1);
735               char *end = g_stpcpy(_base_value, base_value);
736               end = g_stpcpy(end, replacement);
737               g_stpcpy(end, found + pattern_length);
738               g_free(base_value);
739               base_value = _base_value;
740             }
741             break;
742           }
743         }
744         g_free(pattern);
745         g_free(replacement);
746       }
747       break;
748     case '^':
749     case ',':
750       /*
751         changing the case:
752 
753         $(parameter^pattern)
754         $(parameter^^pattern)
755         $(parameter,pattern)
756         $(parameter,,pattern)
757           This expansion modifies the case of alphabetic characters in parameter.
758           The ‘^’ operator converts lowercase letters to uppercase;
759           the ‘,’ operator converts uppercase letters to lowercase.
760           The ‘^^’ and ‘,,’ expansions convert each character in the expanded value;
761           the ‘^’ and ‘,’ expansions convert only the first character in the expanded value.
762       */
763       {
764         const char mode = **variable;
765         char *_base_value = NULL;
766         if(operation == '^' && mode == '^')
767         {
768           _base_value = g_utf8_strup (base_value, -1);
769           (*variable)++;
770         }
771         else if(operation == ',' && mode == ',')
772         {
773           _base_value = g_utf8_strdown(base_value, -1);
774           (*variable)++;
775         }
776         else
777         {
778           gunichar changed = g_utf8_get_char(base_value);
779           changed = operation == '^' ? g_unichar_toupper(changed) : g_unichar_tolower(changed);
780           int utf8_length = g_unichar_to_utf8(changed, NULL);
781           char *next = g_utf8_next_char(base_value);
782           _base_value = g_malloc0(base_value_length - (next - base_value) + utf8_length + 1);
783           g_unichar_to_utf8(changed, _base_value);
784           g_stpcpy(_base_value + utf8_length, next);
785         }
786         g_free(base_value);
787         base_value = _base_value;
788       }
789       break;
790   }
791 
792   if(**variable == ')')
793     (*variable)++;
794   else
795   {
796     // error case
797     g_free(base_value);
798     base_value = NULL;
799   }
800 
801   return base_value;
802 }
803 
grow_buffer(char ** result,char ** result_iter,size_t * result_length,size_t extra_space)804 static void grow_buffer(char **result, char **result_iter, size_t *result_length, size_t extra_space)
805 {
806   const size_t used_length = *result_iter - *result;
807   if(used_length + extra_space > *result_length)
808   {
809     *result_length = used_length + extra_space;
810     *result = g_realloc(*result, *result_length + 1);
811     *result_iter = *result + used_length;
812   }
813 }
814 
expand(dt_variables_params_t * params,char ** source,char extra_stop)815 static char *expand(dt_variables_params_t *params, char **source, char extra_stop)
816 {
817   char *result = g_strdup("");
818   if(!*source) return result;
819   char *result_iter = result;
820   size_t result_length = 0;
821   char *source_iter = *source;
822   const size_t source_length = strlen(*source);
823 
824   while(*source_iter && *source_iter != extra_stop)
825   {
826     // find start of variable, copying over everything till then
827     while(*source_iter && *source_iter != extra_stop)
828     {
829       char c = *source_iter;
830       if(c == '\\' && source_iter[1])
831         c = *(++source_iter);
832       else if(c == '$' && source_iter[1] == '(')
833         break;
834 
835       if(result_iter - result >= result_length)
836         grow_buffer(&result, &result_iter, &result_length, source_length - (source_iter - *source));
837       *result_iter = c;
838       result_iter++;
839       source_iter++;
840 
841     }
842 
843     // it seems we have a variable here
844     if(*source_iter == '$')
845     {
846       char *old_source_iter = source_iter;
847       char *replacement = variable_get_value(params, &source_iter);
848       if(replacement)
849       {
850         const size_t replacement_length = strlen(replacement);
851         grow_buffer(&result, &result_iter, &result_length, replacement_length);
852         memcpy(result_iter, replacement, replacement_length);
853         result_iter += replacement_length;
854         g_free(replacement);
855       }
856       else
857       {
858         // the error case of missing closing ')' -- try to recover
859         source_iter = old_source_iter;
860         grow_buffer(&result, &result_iter, &result_length, source_length - (source_iter - *source));
861         *result_iter++ = *source_iter++;
862       }
863     }
864   }
865 
866   *result_iter = '\0';
867   *source = source_iter;
868 
869   return result;
870 }
871 
dt_variables_expand(dt_variables_params_t * params,gchar * source,gboolean iterate)872 char *dt_variables_expand(dt_variables_params_t *params, gchar *source, gboolean iterate)
873 {
874   init_expansion(params, iterate);
875 
876   char *result = expand(params, &source, '\0');
877 
878   cleanup_expansion(params);
879 
880   return result;
881 }
882 
dt_variables_params_init(dt_variables_params_t ** params)883 void dt_variables_params_init(dt_variables_params_t **params)
884 {
885   *params = g_malloc0(sizeof(dt_variables_params_t));
886   (*params)->data = g_malloc0(sizeof(dt_variables_data_t));
887   time_t now = time(NULL);
888   localtime_r(&now, &(*params)->data->time);
889   (*params)->data->exif_time = 0;
890   (*params)->sequence = -1;
891   (*params)->img = NULL;
892 }
893 
dt_variables_params_destroy(dt_variables_params_t * params)894 void dt_variables_params_destroy(dt_variables_params_t *params)
895 {
896   g_free(params->data);
897   g_free(params);
898 }
899 
dt_variables_set_max_width_height(dt_variables_params_t * params,int max_width,int max_height)900 void dt_variables_set_max_width_height(dt_variables_params_t *params, int max_width, int max_height)
901 {
902   params->data->max_width = max_width;
903   params->data->max_height = max_height;
904 }
905 
dt_variables_set_time(dt_variables_params_t * params,time_t time)906 void dt_variables_set_time(dt_variables_params_t *params, time_t time)
907 {
908   localtime_r(&time, &params->data->time);
909 }
910 
dt_variables_set_exif_time(dt_variables_params_t * params,time_t exif_time)911 void dt_variables_set_exif_time(dt_variables_params_t *params, time_t exif_time)
912 {
913   params->data->exif_time = exif_time;
914 }
915 
dt_variables_reset_sequence(dt_variables_params_t * params)916 void dt_variables_reset_sequence(dt_variables_params_t *params)
917 {
918   params->data->sequence = 0;
919 }
920 
dt_variables_set_tags_flags(dt_variables_params_t * params,uint32_t flags)921 void dt_variables_set_tags_flags(dt_variables_params_t *params, uint32_t flags)
922 {
923   params->data->tags_flags = flags;
924 }
925 
926 
927 
928 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
929 // vim: shiftwidth=2 expandtab tabstop=2 cindent
930 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
931