brewery
notashelf /
5ba4407367ee95b5861d396a7e175731fd6b47bc

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/config.cC620 lines20.9 KB
1
#include <ctype.h>
2
#include <errno.h>
3
#include <stdio.h>
4
#include <stdlib.h>
5
#include <string.h>
6
#include <strings.h>
7
8
#include "chroma.h"
9
#include "vendor/tomlc17.h"
10
11
// Match output name/description against a config pattern
12
// Supports:
13
//   - Exact name match: "DP-1" matches wl_output.name == "DP-1"
14
//   - Description prefix match: "desc:Samsung" matches if description starts
15
//   with "Samsung"
16
static bool match_output(const char *pattern, const char *output_name,
17
                         const char *output_description) {
18
  if (!pattern || !output_name) {
19
    return false;
20
  }
21
22
  // Try exact name match first
23
  if (strcmp(pattern, output_name) == 0) {
24
    return true;
25
  }
26
27
  // Check for description prefix match: "desc:<prefix>"
28
  if (strncmp(pattern, "desc:", 5) == 0) {
29
    const char *desc_prefix = pattern + 5;
30
    size_t prefix_len = strlen(desc_prefix);
31
32
    if (output_description && prefix_len > 0) {
33
      // Match if description starts with the prefix (case-insensitive)
34
      if (strncasecmp(output_description, desc_prefix, prefix_len) == 0) {
35
        return true;
36
      }
37
    }
38
  }
39
40
  return false;
41
}
42
43
// Parse scaling mode from string
44
chroma_scale_mode_t parse_scale_mode(const char *value) {
45
  if (!value)
46
    return CHROMA_SCALE_FILL; // default
47
48
  if (strcasecmp(value, "fill") == 0) {
49
    return CHROMA_SCALE_FILL;
50
  } else if (strcasecmp(value, "fit") == 0) {
51
    return CHROMA_SCALE_FIT;
52
  } else if (strcasecmp(value, "stretch") == 0) {
53
    return CHROMA_SCALE_STRETCH;
54
  } else if (strcasecmp(value, "center") == 0) {
55
    return CHROMA_SCALE_CENTER;
56
  }
57
58
  chroma_log("WARN", "Unknown scaling mode: %s (using fill)", value);
59
  return CHROMA_SCALE_FILL;
60
}
61
62
// Parse filter quality from string
63
chroma_filter_quality_t parse_filter_quality(const char *value) {
64
  if (!value)
65
    return CHROMA_FILTER_LINEAR; // default
66
67
  if (strcasecmp(value, "nearest") == 0) {
68
    return CHROMA_FILTER_NEAREST;
69
  } else if (strcasecmp(value, "linear") == 0) {
70
    return CHROMA_FILTER_LINEAR;
71
  } else if (strcasecmp(value, "bilinear") == 0) {
72
    return CHROMA_FILTER_BILINEAR;
73
  } else if (strcasecmp(value, "trilinear") == 0) {
74
    return CHROMA_FILTER_TRILINEAR;
75
  }
76
77
  chroma_log("WARN", "Unknown filter quality: %s (using linear)", value);
78
  return CHROMA_FILTER_LINEAR;
79
}
80
81
// Get string representation of scaling mode
82
static const char *scale_mode_to_string(chroma_scale_mode_t mode) {
83
  switch (mode) {
84
  case CHROMA_SCALE_FILL:
85
    return "fill";
86
  case CHROMA_SCALE_FIT:
87
    return "fit";
88
  case CHROMA_SCALE_STRETCH:
89
    return "stretch";
90
  case CHROMA_SCALE_CENTER:
91
    return "center";
92
  default:
93
    return "unknown";
94
  }
95
}
96
97
// Get string representation of filter quality
98
static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
99
  switch (quality) {
100
  case CHROMA_FILTER_NEAREST:
101
    return "nearest";
102
  case CHROMA_FILTER_LINEAR:
103
    return "linear";
104
  case CHROMA_FILTER_BILINEAR:
105
    return "bilinear";
106
  case CHROMA_FILTER_TRILINEAR:
107
    return "trilinear";
108
  default:
109
    return "unknown";
110
  }
111
}
112
113
// Parse anchor position from string
114
chroma_anchor_t parse_anchor(const char *value) {
115
  if (!value)
116
    return CHROMA_ANCHOR_CENTER;
117
118
  if (strcasecmp(value, "center") == 0) {
119
    return CHROMA_ANCHOR_CENTER;
120
  } else if (strcasecmp(value, "top") == 0) {
121
    return CHROMA_ANCHOR_TOP;
122
  } else if (strcasecmp(value, "bottom") == 0) {
123
    return CHROMA_ANCHOR_BOTTOM;
124
  } else if (strcasecmp(value, "left") == 0) {
125
    return CHROMA_ANCHOR_LEFT;
126
  } else if (strcasecmp(value, "right") == 0) {
127
    return CHROMA_ANCHOR_RIGHT;
128
  } else if (strcasecmp(value, "top-left") == 0 ||
129
             strcasecmp(value, "topleft") == 0) {
130
    return CHROMA_ANCHOR_TOP_LEFT;
131
  } else if (strcasecmp(value, "top-right") == 0 ||
132
             strcasecmp(value, "topright") == 0) {
133
    return CHROMA_ANCHOR_TOP_RIGHT;
134
  } else if (strcasecmp(value, "bottom-left") == 0 ||
135
             strcasecmp(value, "bottomleft") == 0) {
136
    return CHROMA_ANCHOR_BOTTOM_LEFT;
137
  } else if (strcasecmp(value, "bottom-right") == 0 ||
138
             strcasecmp(value, "bottomright") == 0) {
139
    return CHROMA_ANCHOR_BOTTOM_RIGHT;
140
  }
141
142
  chroma_log("WARN", "Unknown anchor: %s (using center)", value);
143
  return CHROMA_ANCHOR_CENTER;
144
}
145
146
// Get string representation of anchor position
147
static const char *anchor_to_string(chroma_anchor_t anchor) {
148
  switch (anchor) {
149
  case CHROMA_ANCHOR_CENTER:
150
    return "center";
151
  case CHROMA_ANCHOR_TOP:
152
    return "top";
153
  case CHROMA_ANCHOR_BOTTOM:
154
    return "bottom";
155
  case CHROMA_ANCHOR_LEFT:
156
    return "left";
157
  case CHROMA_ANCHOR_RIGHT:
158
    return "right";
159
  case CHROMA_ANCHOR_TOP_LEFT:
160
    return "top-left";
161
  case CHROMA_ANCHOR_TOP_RIGHT:
162
    return "top-right";
163
  case CHROMA_ANCHOR_BOTTOM_LEFT:
164
    return "bottom-left";
165
  case CHROMA_ANCHOR_BOTTOM_RIGHT:
166
    return "bottom-right";
167
  default:
168
    return "unknown";
169
  }
170
}
171
172
// Output-to-image mapping
173
int add_output_mapping(chroma_config_t *config, const char *output_name,
174
                       const char *image_path, chroma_scale_mode_t scale_mode,
175
                       chroma_filter_quality_t filter_quality,
176
                       chroma_anchor_t anchor, float anchor_x, float anchor_y) {
177
  if (!config || !output_name || !image_path) {
178
    return CHROMA_ERROR_INIT;
179
  }
180
181
  if (config->mapping_count >= MAX_OUTPUTS) {
182
    chroma_log("ERROR", "Maximum number of output mappings reached (%d)",
183
               MAX_OUTPUTS);
184
    return CHROMA_ERROR_MEMORY;
185
  }
186
187
  // XXX: Validate string lengths to prevent buffer overflow
188
  size_t output_len = strlen(output_name);
189
  size_t path_len = strlen(image_path);
190
191
  if (output_len >= sizeof(config->mappings[0].output_name)) {
192
    chroma_log("ERROR", "Output name too long: %s (max %zu)", output_name,
193
               sizeof(config->mappings[0].output_name) - 1);
194
    return CHROMA_ERROR_CONFIG;
195
  }
196
197
  if (path_len >= sizeof(config->mappings[0].image_path)) {
198
    chroma_log("ERROR", "Image path too long: %s (max %zu)", image_path,
199
               sizeof(config->mappings[0].image_path) - 1);
200
    return CHROMA_ERROR_CONFIG;
201
  }
202
203
  chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
204
205
  strcpy(mapping->output_name, output_name);
206
  strcpy(mapping->image_path, image_path);
207
  mapping->scale_mode = scale_mode;
208
  mapping->filter_quality = filter_quality;
209
  mapping->anchor = anchor;
210
  mapping->anchor_x = anchor_x;
211
  mapping->anchor_y = anchor_y;
212
213
  config->mapping_count++;
214
215
  chroma_log(
216
      "DEBUG",
217
      "Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s @ %.1f,%.1f)",
218
      output_name, image_path, scale_mode_to_string(scale_mode),
219
      filter_quality_to_string(filter_quality), anchor_to_string(anchor),
220
      (double)anchor_x, (double)anchor_y);
221
  chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
222
             config->mapping_count, output_name, image_path, path_len);
223
  return CHROMA_OK;
224
}
225
226
// Parse TOML configuration file
227
int chroma_config_load_toml(chroma_config_t *config, const char *config_file) {
228
  if (!config || !config_file) {
229
    return CHROMA_ERROR_INIT;
230
  }
231
232
  toml_result_t result = toml_parse_file_ex(config_file);
233
  if (!result.ok) {
234
    chroma_log("DEBUG", "TOML parse failed: %s", result.errmsg);
235
    toml_free(result);
236
    return CHROMA_ERROR_CONFIG;
237
  }
238
239
  chroma_log("INFO", "Loading TOML configuration from: %s", config_file);
240
241
  // Parse default_image
242
  toml_datum_t default_image = toml_seek(result.toptab, "default_image");
243
  if (default_image.type == TOML_STRING) {
244
    char *expanded_path = chroma_expand_path(default_image.u.s);
245
    const char *path_to_use = expanded_path ? expanded_path : default_image.u.s;
246
    if (strlen(path_to_use) < sizeof(config->default_image)) {
247
      strcpy(config->default_image, path_to_use);
248
    } else {
249
      chroma_log(
250
          "WARN", "default_image path too long (%zu >= %zu), truncating: %s",
251
          strlen(path_to_use), sizeof(config->default_image), path_to_use);
252
      snprintf(config->default_image, sizeof(config->default_image), "%s",
253
               path_to_use);
254
    }
255
    if (expanded_path) {
256
      free(expanded_path);
257
    }
258
  }
259
260
  // Parse daemon_mode
261
  toml_datum_t daemon_mode = toml_seek(result.toptab, "daemon_mode");
262
  if (daemon_mode.type == TOML_BOOLEAN) {
263
    config->daemon_mode = daemon_mode.u.boolean;
264
  }
265
266
  // Parse scale_mode
267
  toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode");
268
  if (scale_mode.type == TOML_STRING) {
269
    config->default_scale_mode = parse_scale_mode(scale_mode.u.s);
270
  }
271
272
  // Parse filter_quality
273
  toml_datum_t filter_quality = toml_seek(result.toptab, "filter_quality");
274
  if (filter_quality.type == TOML_STRING) {
275
    config->default_filter_quality = parse_filter_quality(filter_quality.u.s);
276
  }
277
278
  // Parse anchor
279
  toml_datum_t anchor = toml_seek(result.toptab, "anchor");
280
  if (anchor.type == TOML_STRING) {
281
    config->default_anchor = parse_anchor(anchor.u.s);
282
  }
283
284
  // Parse anchor_x
285
  toml_datum_t anchor_x = toml_seek(result.toptab, "anchor_x");
286
  if (anchor_x.type == TOML_INT64) {
287
    config->default_anchor_x = (float)anchor_x.u.int64;
288
  } else if (anchor_x.type == TOML_FP64) {
289
    config->default_anchor_x = (float)anchor_x.u.fp64;
290
  }
291
292
  // Parse anchor_y
293
  toml_datum_t anchor_y = toml_seek(result.toptab, "anchor_y");
294
  if (anchor_y.type == TOML_INT64) {
295
    config->default_anchor_y = (float)anchor_y.u.int64;
296
  } else if (anchor_y.type == TOML_FP64) {
297
    config->default_anchor_y = (float)anchor_y.u.fp64;
298
  }
299
300
  // Parse downsampling section
301
  toml_datum_t downsampling = toml_seek(result.toptab, "downsampling");
302
  if (downsampling.type == TOML_TABLE) {
303
    toml_datum_t ds_tab = downsampling;
304
305
    toml_datum_t enable = toml_seek(ds_tab, "enable");
306
    if (enable.type == TOML_BOOLEAN) {
307
      config->enable_downsampling = enable.u.boolean;
308
    }
309
310
    toml_datum_t max_width = toml_seek(ds_tab, "max_output_width");
311
    if (max_width.type == TOML_INT64) {
312
      config->max_output_width = (int)max_width.u.int64;
313
    }
314
315
    toml_datum_t max_height = toml_seek(ds_tab, "max_output_height");
316
    if (max_height.type == TOML_INT64) {
317
      config->max_output_height = (int)max_height.u.int64;
318
    }
319
320
    toml_datum_t min_scale = toml_seek(ds_tab, "min_scale_factor");
321
    if (min_scale.type == TOML_FP64) {
322
      config->min_scale_factor = (float)min_scale.u.fp64;
323
    } else if (min_scale.type == TOML_INT64) {
324
      config->min_scale_factor = (float)min_scale.u.int64;
325
    }
326
  }
327
328
  // Parse output mappings array
329
  toml_datum_t outputs = toml_seek(result.toptab, "output");
330
  if (outputs.type == TOML_ARRAY) {
331
    for (int i = 0; i < outputs.u.arr.size; i++) {
332
      toml_datum_t output = outputs.u.arr.elem[i];
333
      if (output.type != TOML_TABLE) {
334
        continue;
335
      }
336
337
      toml_datum_t output_tab = output;
338
339
      // Get output name
340
      toml_datum_t name = toml_seek(output_tab, "name");
341
      if (name.type != TOML_STRING) {
342
        chroma_log("WARN", "Output mapping %d missing name", i);
343
        continue;
344
      }
345
346
      // Get image path
347
      toml_datum_t image = toml_seek(output_tab, "image");
348
      if (image.type != TOML_STRING) {
349
        chroma_log("WARN", "Output mapping %d missing image", i);
350
        continue;
351
      }
352
353
      char *expanded_path = chroma_expand_path(image.u.s);
354
      const char *path_to_use = expanded_path ? expanded_path : image.u.s;
355
356
      // Get optional settings with defaults from global config
357
      chroma_scale_mode_t out_scale = config->default_scale_mode;
358
      chroma_filter_quality_t out_filter = config->default_filter_quality;
359
      chroma_anchor_t out_anchor = config->default_anchor;
360
      float out_anchor_x = config->default_anchor_x;
361
      float out_anchor_y = config->default_anchor_y;
362
363
      toml_datum_t out_scale_val = toml_seek(output_tab, "scale");
364
      if (out_scale_val.type == TOML_STRING) {
365
        out_scale = parse_scale_mode(out_scale_val.u.s);
366
      }
367
368
      toml_datum_t out_filter_val = toml_seek(output_tab, "filter");
369
      if (out_filter_val.type == TOML_STRING) {
370
        out_filter = parse_filter_quality(out_filter_val.u.s);
371
      }
372
373
      toml_datum_t out_anchor_val = toml_seek(output_tab, "anchor");
374
      if (out_anchor_val.type == TOML_STRING) {
375
        out_anchor = parse_anchor(out_anchor_val.u.s);
376
      }
377
378
      toml_datum_t out_anchor_x_val = toml_seek(output_tab, "anchor_x");
379
      if (out_anchor_x_val.type == TOML_INT64) {
380
        out_anchor_x = (float)out_anchor_x_val.u.int64;
381
      } else if (out_anchor_x_val.type == TOML_FP64) {
382
        out_anchor_x = (float)out_anchor_x_val.u.fp64;
383
      }
384
385
      toml_datum_t out_anchor_y_val = toml_seek(output_tab, "anchor_y");
386
      if (out_anchor_y_val.type == TOML_INT64) {
387
        out_anchor_y = (float)out_anchor_y_val.u.int64;
388
      } else if (out_anchor_y_val.type == TOML_FP64) {
389
        out_anchor_y = (float)out_anchor_y_val.u.fp64;
390
      }
391
392
      // Add the mapping
393
      if (add_output_mapping(config, name.u.s, path_to_use, out_scale,
394
                             out_filter, out_anchor, out_anchor_x,
395
                             out_anchor_y) != CHROMA_OK) {
396
        chroma_log("ERROR", "Failed to add TOML output mapping: %s", name.u.s);
397
      }
398
399
      if (expanded_path) {
400
        free(expanded_path);
401
      }
402
    }
403
  }
404
405
  toml_free(result);
406
407
  chroma_log("INFO",
408
             "Loaded TOML configuration: %d output mappings, default image: %s",
409
             config->mapping_count, config->default_image);
410
411
  return CHROMA_OK;
412
}
413
414
// Initialize configuration with defaults
415
static void init_default_config(chroma_config_t *config) {
416
  if (!config)
417
    return;
418
419
  memset(config, 0, sizeof(chroma_config_t));
420
421
  config->daemon_mode = false;
422
  config->mapping_count = 0;
423
424
  // Set default scaling and filtering
425
  config->default_scale_mode = CHROMA_SCALE_FILL;
426
  config->default_filter_quality = CHROMA_FILTER_LINEAR;
427
  config->default_anchor = CHROMA_ANCHOR_CENTER;
428
  config->default_anchor_x = 50.0f; // center
429
  config->default_anchor_y = 50.0f; // center
430
431
  // Set default downsampling settings
432
  config->enable_downsampling = true; // enable by default, performance etc.
433
  config->max_output_width = 3840;    // 4K width
434
  config->max_output_height = 2160;   // 4K height
435
  config->min_scale_factor = 0.25f;   // don't scale below 25%
436
437
  // Leave default_image empty - user must configure it explicitly
438
  // This avoids errors when the hardcoded path doesn't exist
439
  config->default_image[0] = '\0';
440
}
441
442
// Load configuration from file (TOML format only)
443
int chroma_config_load(chroma_config_t *config, const char *config_file) {
444
  if (!config) {
445
    return CHROMA_ERROR_INIT;
446
  }
447
448
  // Initialize with defaults
449
  init_default_config(config);
450
451
  if (!config_file) {
452
    chroma_log("INFO", "No config file specified, using defaults");
453
    return CHROMA_OK;
454
  }
455
456
  // Check if file exists
457
  if (!chroma_path_exists(config_file)) {
458
    chroma_log("INFO", "Config file not found: %s (using defaults)",
459
               config_file);
460
    return CHROMA_OK;
461
  }
462
463
  // Load TOML configuration
464
  int result = chroma_config_load_toml(config, config_file);
465
  if (result != CHROMA_OK) {
466
    chroma_log("ERROR", "Failed to load TOML config from %s", config_file);
467
    return result;
468
  }
469
470
  // Log configuration memory usage
471
  size_t config_size =
472
      sizeof(chroma_config_t) +
473
      ((size_t)config->mapping_count * sizeof(chroma_config_mapping_t));
474
  chroma_log_resource_allocation("config_data", config_size,
475
                                 "configuration structure");
476
  chroma_log_memory_stats("post-config-load");
477
478
  return CHROMA_OK;
479
}
480
481
// Free configuration resources
482
void chroma_config_free(chroma_config_t *config) {
483
  if (!config) {
484
    return;
485
  }
486
487
  // Log configuration deallocation
488
  size_t config_size =
489
      sizeof(chroma_config_t) +
490
      ((size_t)config->mapping_count * sizeof(chroma_config_mapping_t));
491
  chroma_log_resource_deallocation("config_data", config_size,
492
                                   "configuration structure");
493
494
  chroma_log("TRACE", "Clearing %d output mappings from configuration",
495
             config->mapping_count);
496
  memset(config->mappings, 0, sizeof(config->mappings));
497
  config->mapping_count = 0;
498
499
  memset(config->default_image, 0, sizeof(config->default_image));
500
501
  chroma_log("DEBUG", "Freed configuration");
502
}
503
504
// Get image path for specific output
505
const char *chroma_config_get_image_for_output(chroma_config_t *config,
506
                                               const char *output_name,
507
                                               const char *output_description) {
508
  if (!config || !output_name) {
509
    return NULL;
510
  }
511
512
  // Look for specific output mapping (name or description match)
513
  for (int i = 0; i < config->mapping_count; i++) {
514
    if (match_output(config->mappings[i].output_name, output_name,
515
                     output_description)) {
516
      chroma_log("DEBUG", "Found specific mapping for output %s (desc: %s): %s",
517
                 output_name, output_description ? output_description : "none",
518
                 config->mappings[i].image_path);
519
      return config->mappings[i].image_path;
520
    }
521
  }
522
523
  // Return default image if no specific mapping found
524
  if (strlen(config->default_image) > 0) {
525
    chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
526
               config->default_image);
527
    return config->default_image;
528
  }
529
530
  chroma_log("WARN", "No image configured for output: %s", output_name);
531
  return NULL;
532
}
533
534
// Get configuration mapping for output, including scale mode, filter
535
// quality, anchor, and custom anchor coordinates
536
int chroma_config_get_mapping_for_output(
537
    chroma_config_t *config, const char *output_name,
538
    const char *output_description, chroma_scale_mode_t *scale_mode,
539
    chroma_filter_quality_t *filter_quality, chroma_anchor_t *anchor,
540
    float *anchor_x, float *anchor_y) {
541
  if (!config || !output_name || !scale_mode || !filter_quality || !anchor ||
542
      !anchor_x || !anchor_y) {
543
    return CHROMA_ERROR_INIT;
544
  }
545
546
  // Look for specific output mapping (name or description match)
547
  for (int i = 0; i < config->mapping_count; i++) {
548
    if (match_output(config->mappings[i].output_name, output_name,
549
                     output_description)) {
550
      *scale_mode = config->mappings[i].scale_mode;
551
      *filter_quality = config->mappings[i].filter_quality;
552
      *anchor = config->mappings[i].anchor;
553
      *anchor_x = config->mappings[i].anchor_x;
554
      *anchor_y = config->mappings[i].anchor_y;
555
      chroma_log("DEBUG",
556
                 "Found specific mapping for output %s (desc: %s): scale=%s, "
557
                 "filter=%s, anchor=%s @ %.1f,%.1f",
558
                 output_name, output_description ? output_description : "none",
559
                 scale_mode_to_string(*scale_mode),
560
                 filter_quality_to_string(*filter_quality),
561
                 anchor_to_string(*anchor), (double)*anchor_x,
562
                 (double)*anchor_y);
563
      return CHROMA_OK;
564
    }
565
  }
566
567
  // Return defaults if no specific mapping found
568
  *scale_mode = config->default_scale_mode;
569
  *filter_quality = config->default_filter_quality;
570
  *anchor = config->default_anchor;
571
  *anchor_x = config->default_anchor_x;
572
  *anchor_y = config->default_anchor_y;
573
  chroma_log("DEBUG",
574
             "Using defaults for output %s: scale=%s, filter=%s, anchor=%s @ "
575
             "%.1f,%.1f",
576
             output_name, scale_mode_to_string(*scale_mode),
577
             filter_quality_to_string(*filter_quality),
578
             anchor_to_string(*anchor), (double)*anchor_x, (double)*anchor_y);
579
  return CHROMA_OK;
580
}
581
582
// Print current configuration for debugging
583
void chroma_config_print(const chroma_config_t *config) {
584
  if (!config) {
585
    return;
586
  }
587
588
  chroma_log("INFO", "=== Configuration ===");
589
  chroma_log("INFO", "Default image: %s", config->default_image);
590
  chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
591
  chroma_log("INFO", "Default scale mode: %s",
592
             scale_mode_to_string(config->default_scale_mode));
593
  chroma_log("INFO", "Default filter quality: %s",
594
             filter_quality_to_string(config->default_filter_quality));
595
  chroma_log("INFO", "Downsampling: %s",
596
             config->enable_downsampling ? "enabled" : "disabled");
597
  if (config->enable_downsampling) {
598
    chroma_log("INFO", "Max output size: %dx%d", config->max_output_width,
599
               config->max_output_height);
600
    chroma_log("INFO", "Min scale factor: %.2f",
601
               (double)config->min_scale_factor);
602
  }
603
  chroma_log("INFO", "Output mappings: %d", config->mapping_count);
604
605
  for (int i = 0; i < config->mapping_count; i++) {
606
    chroma_log("INFO", "  %s -> %s (scale: %s, filter: %s)",
607
               config->mappings[i].output_name, config->mappings[i].image_path,
608
               scale_mode_to_string(config->mappings[i].scale_mode),
609
               filter_quality_to_string(config->mappings[i].filter_quality));
610
    chroma_log(
611
        "TRACE",
612
        "  Mapping %d: output='%s', image='%s', scale='%s', filter='%s', "
613
        "path_exists=%s",
614
        i, config->mappings[i].output_name, config->mappings[i].image_path,
615
        scale_mode_to_string(config->mappings[i].scale_mode),
616
        filter_quality_to_string(config->mappings[i].filter_quality),
617
        chroma_path_exists(config->mappings[i].image_path) ? "yes" : "no");
618
  }
619
  chroma_log("INFO", "====================");
620
}