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", ¶ms->data->exif_tm.tm_year, ¶ms->data->exif_tm.tm_mon,
119 ¶ms->data->exif_tm.tm_mday, ¶ms->data->exif_tm.tm_hour, ¶ms->data->exif_tm.tm_min, ¶ms->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(¶ms->data->exif_time, ¶ms->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, ¶ms->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