• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saitoha / libsixel / 25760399605

12 May 2026 08:22PM UTC coverage: 86.779% (+0.009%) from 86.77%
25760399605

push

github

saitoha
test: align suboption short syntax TAP wording

127461 of 257328 branches covered (49.53%)

153175 of 176511 relevant lines covered (86.78%)

19016109.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.03
/src/decoder.c
1
/*
2
 * SPDX-License-Identifier: MIT
3
 *
4
 * Copyright (c) 2014-2025 Hayaki Saito
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
7
 * this software and associated documentation files (the "Software"), to deal in
8
 * the Software without restriction, including without limitation the rights to
9
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
 * the Software, and to permit persons to whom the Software is furnished to do so,
11
 * subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in all
14
 * copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 */
23

24
#if defined(HAVE_CONFIG_H)
25
#include "config.h"
26
#endif
27

28
/* STDC_HEADERS */
29
#include <stdio.h>
30
#include <stdlib.h>
31
#include <string.h>
32

33
#if HAVE_MATH_H
34
# include <math.h>
35
#endif  /* HAVE_MATH_H */
36
#if HAVE_LIMITS_H
37
# include <limits.h>
38
#endif  /* HAVE_LIMITS_H */
39
#if HAVE_UNISTD_H
40
# include <unistd.h>
41
#elif HAVE_SYS_UNISTD_H
42
# include <sys/unistd.h>
43
#endif  /* HAVE_UNISTD_H */
44
#if HAVE_FCNTL_H
45
# include <fcntl.h>
46
#endif  /* HAVE_FCNTL_H */
47
#if HAVE_SYS_STAT_H
48
# include <sys/stat.h>
49
#endif  /* HAVE_SYS_STAT_H */
50
#if HAVE_ERRNO_H
51
# include <errno.h>
52
#endif  /* HAVE_ERRNO_H */
53
#if HAVE_IO_H
54
#include <io.h>
55
#endif  /* HAVE_IO_H */
56

57
#include "decoder.h"
58
#include "decoder-parallel.h"
59
#include "frame-factory.h"
60
#include "clipboard.h"
61
#include "compat_stub.h"
62
#include "path.h"
63
#include "options.h"
64
#include "sixel_atomic.h"
65

66
static void
67
decoder_clipboard_select_format(char *dest,
178✔
68
                                size_t dest_size,
69
                                char const *format,
70
                                char const *fallback)
71
{
72
    char const *source;
90✔
73
    size_t limit;
90✔
74

75
    if (dest == NULL || dest_size == 0u) {
178!
76
        return;
77
    }
78

79
    source = fallback;
178✔
80
    if (format != NULL && format[0] != '\0') {
178!
81
        source = format;
50✔
82
    }
83

84
    limit = dest_size - 1u;
178✔
85
    if (limit == 0u) {
178!
86
        dest[0] = '\0';
×
87
        return;
×
88
    }
89

90
    (void)snprintf(dest, dest_size, "%.*s", (int)limit, source);
178✔
91
}
72!
92

93

94
static char *
95
decoder_create_temp_template_with_prefix(sixel_allocator_t *allocator,
96✔
96
                                         char const *prefix,
97
                                         size_t *capacity_out)
98
{
99
    char const *tmpdir;
48✔
100
    size_t tmpdir_len;
48✔
101
    size_t prefix_len;
48✔
102
    size_t suffix_len;
48✔
103
    size_t template_len;
48✔
104
    char *template_path;
48✔
105
    int needs_separator;
48✔
106
    size_t maximum_tmpdir_len;
48✔
107

108
#if defined(_WIN32)
109
    /*
110
     * MinGW runtimes under Wine can reject host-side TMPDIR values
111
     * (for example "/home/..."). Prefer TEMP/TMP first and then
112
     * fall back to TMPDIR.
113
     */
114
    tmpdir = sixel_compat_getenv("TEMP");
36✔
115
    if (tmpdir == NULL || tmpdir[0] == '\0') {
36✔
116
        tmpdir = sixel_compat_getenv("TMP");
117
    }
118
    if (tmpdir == NULL || tmpdir[0] == '\0') {
36✔
119
        tmpdir = sixel_compat_getenv("TMPDIR");
120
    }
121
#else
122
    tmpdir = sixel_compat_getenv("TMPDIR");
60✔
123
#endif
124
    if (tmpdir == NULL || tmpdir[0] == '\0') {
96!
125
#if defined(_WIN32)
126
        tmpdir = ".";
127
#else
128
        tmpdir = "/tmp";
28✔
129
#endif
130
    }
131

132
    tmpdir_len = strlen(tmpdir);
96✔
133
    prefix_len = strlen(prefix);
96✔
134
    suffix_len = prefix_len + strlen("-XXXXXX");
96✔
135
    maximum_tmpdir_len = (size_t)INT_MAX;
96✔
136

137
    if (maximum_tmpdir_len <= suffix_len + 2) {
96!
138
        return NULL;
139
    }
140
    if (tmpdir_len > maximum_tmpdir_len - (suffix_len + 2)) {
96!
141
        return NULL;
142
    }
143

144
    needs_separator = 1;
96✔
145
    if (tmpdir_len > 0) {
96!
146
        if (tmpdir[tmpdir_len - 1] == '/' || tmpdir[tmpdir_len - 1] == '\\') {
96!
147
            needs_separator = 0;
60✔
148
        }
32✔
149
    }
36✔
150

151
    template_len = tmpdir_len + suffix_len + 2;
96✔
152
    template_path = (char *)sixel_allocator_malloc(allocator, template_len);
96✔
153
    if (template_path == NULL) {
96!
154
        return NULL;
155
    }
156

157
    if (needs_separator) {
96!
158
#if defined(_WIN32)
159
        (void)snprintf(template_path, template_len,
40✔
160
                       "%s\\%s-XXXXXX", tmpdir, prefix);
4✔
161
#else
162
        (void)snprintf(template_path, template_len,
28✔
163
                       "%s/%s-XXXXXX", tmpdir, prefix);
164
#endif
165
    } else {
4✔
166
        (void)snprintf(template_path, template_len,
32✔
167
                       "%s%s-XXXXXX", tmpdir, prefix);
168
    }
169

170
    if (capacity_out != NULL) {
96!
171
        *capacity_out = template_len;
96✔
172
    }
36✔
173

174
    return template_path;
68✔
175
}
36✔
176

177

178
static SIXELSTATUS
179
decoder_clipboard_create_spool(sixel_allocator_t *allocator,
96✔
180
                               char const *prefix,
181
                               char **path_out)
182
{
183
    SIXELSTATUS status;
48✔
184
    char *template_path;
48✔
185
    size_t template_capacity;
48✔
186
    int open_flags;
48✔
187
    int open_mode;
48✔
188
    int open_attempt;
48✔
189
    int open_errno;
48✔
190
    int fd;
48✔
191
    char *tmpname_result;
48✔
192

193
    status = SIXEL_FALSE;
96✔
194
    template_path = NULL;
96✔
195
    template_capacity = 0u;
96✔
196
    open_flags = 0;
96✔
197
    open_mode = 0;
96✔
198
    fd = (-1);
96✔
199
    tmpname_result = NULL;
96✔
200

201
    template_path = decoder_create_temp_template_with_prefix(allocator,
132✔
202
                                                             prefix,
36✔
203
                                                             &template_capacity);
204
    if (template_path == NULL) {
96!
205
        sixel_helper_set_additional_message(
×
206
            "clipboard: failed to allocate spool template.");
207
        status = SIXEL_BAD_ALLOCATION;
×
208
        goto end;
×
209
    }
210

211
    if (sixel_compat_mktemp(template_path, template_capacity) != 0) {
96!
212
        tmpname_result = sixel_compat_tmpnam(template_path,
×
213
                                             template_capacity);
214
        if (tmpname_result == NULL) {
×
215
            sixel_helper_set_additional_message(
×
216
                "clipboard: failed to reserve spool template.");
217
            status = SIXEL_LIBC_ERROR;
×
218
            goto end;
×
219
        }
220
        template_capacity = strlen(template_path) + 1u;
×
221
    }
222

223
    open_flags = O_RDWR | O_CREAT | O_TRUNC;
68✔
224
#if defined(O_BINARY)
225
    open_flags |= O_BINARY;
24✔
226
#endif
227
    /*
228
     * Emscripten + NODERAWFS can report false EEXIST for O_EXCL on freshly
229
     * generated temp paths. Keep O_EXCL on native runtimes and drop it for
230
     * emscripten to preserve clipboard spool creation reliability.
231
     */
232
#if defined(O_EXCL) && !defined(__EMSCRIPTEN__)
233
    open_flags |= O_EXCL;
96✔
234
#endif
235
    open_mode = S_IRUSR | S_IWUSR;
96✔
236
    open_errno = 0;
68✔
237
    open_attempt = 0;
68✔
238
    for (open_attempt = 0; open_attempt < 4; ++open_attempt) {
96!
239
        fd = sixel_compat_open(template_path, open_flags, open_mode);
96✔
240
        if (fd >= 0) {
96✔
241
            break;
68✔
242
        }
243
        open_errno = errno;
×
244
        if (open_errno != EEXIST) {
×
245
            break;
246
        }
247
        /*
248
         * Emscripten mktemp implementations can return reused names.
249
         * Regenerate the path and retry when the generated file already exists.
250
         */
251
        if (sixel_compat_mktemp(template_path, template_capacity) != 0) {
×
252
            tmpname_result = sixel_compat_tmpnam(template_path,
×
253
                                                 template_capacity);
254
            if (tmpname_result == NULL) {
×
255
                sixel_helper_set_additional_message(
×
256
                    "clipboard: failed to reserve spool template.");
257
                status = SIXEL_LIBC_ERROR;
×
258
                goto end;
×
259
            }
260
            template_capacity = strlen(template_path) + 1u;
×
261
        }
262
    }
263
    if (fd < 0) {
96!
264
        if (open_errno != 0) {
×
265
            errno = open_errno;
×
266
        }
267
        sixel_helper_set_additional_message(
×
268
            "clipboard: failed to open spool file.");
269
        status = SIXEL_LIBC_ERROR;
×
270
        goto end;
×
271
    }
272

273
    *path_out = template_path;
96✔
274
    if (fd >= 0) {
96!
275
        (void)sixel_compat_close(fd);
96✔
276
        fd = (-1);
96✔
277
    }
36✔
278

279
    template_path = NULL;
96✔
280
    status = SIXEL_OK;
96✔
281

282
end:
32✔
283
    if (fd >= 0) {
96!
284
        (void)sixel_compat_close(fd);
285
    }
286
    if (template_path != NULL) {
92!
287
        sixel_allocator_free(allocator, template_path);
×
288
    }
289

290
    return status;
116✔
291
}
20✔
292

293

294
static SIXELSTATUS
295
decoder_clipboard_read_file(char const *path,
96✔
296
                            unsigned char **data,
297
                            size_t *size)
298
{
299
    FILE *stream;
48✔
300
    long seek_result;
48✔
301
    long file_size;
48✔
302
    unsigned char *buffer;
48✔
303
    size_t read_size;
48✔
304

305
    if (data == NULL || size == NULL) {
96!
306
        sixel_helper_set_additional_message(
×
307
            "clipboard: read buffer pointers are null.");
308
        return SIXEL_BAD_ARGUMENT;
×
309
    }
310

311
    *data = NULL;
96✔
312
    *size = 0u;
96✔
313

314
    if (path == NULL) {
96!
315
        sixel_helper_set_additional_message(
×
316
            "clipboard: spool path is null.");
317
        return SIXEL_BAD_ARGUMENT;
×
318
    }
319

320
    stream = sixel_compat_fopen(path, "rb");
96✔
321
    if (stream == NULL) {
96!
322
        sixel_helper_set_additional_message(
×
323
            "clipboard: failed to open spool file for read.");
324
        return SIXEL_LIBC_ERROR;
×
325
    }
326

327
    seek_result = fseek(stream, 0L, SEEK_END);
96✔
328
    if (seek_result != 0) {
96!
329
        (void)fclose(stream);
×
330
        sixel_helper_set_additional_message(
×
331
            "clipboard: failed to seek spool file.");
332
        return SIXEL_LIBC_ERROR;
×
333
    }
334

335
    file_size = ftell(stream);
96✔
336
    if (file_size < 0) {
96!
337
        (void)fclose(stream);
×
338
        sixel_helper_set_additional_message(
×
339
            "clipboard: failed to determine spool size.");
340
        return SIXEL_LIBC_ERROR;
×
341
    }
342

343
    seek_result = fseek(stream, 0L, SEEK_SET);
96✔
344
    if (seek_result != 0) {
96!
345
        (void)fclose(stream);
×
346
        sixel_helper_set_additional_message(
×
347
            "clipboard: failed to rewind spool file.");
348
        return SIXEL_LIBC_ERROR;
×
349
    }
350

351
    if (file_size == 0) {
96!
352
        buffer = NULL;
353
        read_size = 0u;
354
    } else {
355
        buffer = (unsigned char *)malloc((size_t)file_size);
96✔
356
        if (buffer == NULL) {
96!
357
            (void)fclose(stream);
×
358
            sixel_helper_set_additional_message(
×
359
                "clipboard: malloc() failed for spool payload.");
360
            return SIXEL_BAD_ALLOCATION;
×
361
        }
362
        read_size = fread(buffer, 1u, (size_t)file_size, stream);
96!
363
        if (read_size != (size_t)file_size) {
96!
364
            free(buffer);
×
365
            (void)fclose(stream);
×
366
            sixel_helper_set_additional_message(
×
367
                "clipboard: failed to read spool payload.");
368
            return SIXEL_LIBC_ERROR;
×
369
        }
370
    }
371

372
    if (fclose(stream) != 0) {
96!
373
        if (buffer != NULL) {
×
374
            free(buffer);
×
375
        }
376
        sixel_helper_set_additional_message(
×
377
            "clipboard: failed to close spool file after read.");
378
        return SIXEL_LIBC_ERROR;
×
379
    }
380

381
    *data = buffer;
96✔
382
    *size = read_size;
96✔
383

384
    return SIXEL_OK;
96✔
385
}
36✔
386

387

388
/* original version of strdup(3) with allocator object */
389
static char *
390
strdup_with_allocator(
12,320✔
391
    char const          /* in */ *s,          /* source buffer */
392
    sixel_allocator_t   /* in */ *allocator)  /* allocator object for
393
                                                 destination buffer */
394
{
395
    char *p;
6,848✔
396

397
    if (s == NULL || allocator == NULL) {
12,320!
398
        return NULL;
399
    }
400
    p = (char *)sixel_allocator_malloc(allocator, (size_t)(strlen(s) + 1));
12,320✔
401
    if (p) {
12,320!
402
        (void)sixel_compat_strcpy(p, strlen(s) + 1, s);
12,320✔
403
    }
6,470✔
404
    return p;
9,590✔
405
}
6,470✔
406

407

408
/* create decoder object */
409
SIXELAPI SIXELSTATUS
410
sixel_decoder_new(
3,427✔
411
    sixel_decoder_t    /* out */ **ppdecoder,  /* decoder object to be created */
412
    sixel_allocator_t  /* in */  *allocator)   /* allocator, null if you use
413
                                                  default allocator */
414
{
415
    SIXELSTATUS status = SIXEL_FALSE;
3,427✔
416

417
    if (allocator == NULL) {
3,427✔
418
        status = sixel_allocator_new(&allocator, NULL, NULL, NULL, NULL);
3,348✔
419
        if (SIXEL_FAILED(status)) {
3,348!
420
            goto end;
×
421
        }
422
    } else {
1,712✔
423
        sixel_allocator_ref(allocator);
79✔
424
    }
425

426
    *ppdecoder = sixel_allocator_malloc(allocator, sizeof(sixel_decoder_t));
3,426✔
427
    if (*ppdecoder == NULL) {
3,428!
428
        sixel_allocator_unref(allocator);
×
429
        sixel_helper_set_additional_message(
×
430
            "sixel_decoder_new: sixel_allocator_malloc() failed.");
431
        status = SIXEL_BAD_ALLOCATION;
×
432
        goto end;
×
433
    }
434

435
    (*ppdecoder)->ref          = 1U;
3,428✔
436
    (*ppdecoder)->output       = strdup_with_allocator("-", allocator);
3,428✔
437
    (*ppdecoder)->input        = strdup_with_allocator("-", allocator);
3,428✔
438
    (*ppdecoder)->allocator    = allocator;
3,428✔
439
    (*ppdecoder)->dequantize_method = SIXEL_DEQUANTIZE_NONE;
3,428✔
440
    (*ppdecoder)->dequantize_similarity_bias = 100;
3,428✔
441
    (*ppdecoder)->dequantize_edge_strength = 0;
3,428✔
442
    (*ppdecoder)->thumbnail_size = 0;
3,428✔
443
    (*ppdecoder)->direct_color = 0;
3,428✔
444
    (*ppdecoder)->clipboard_input_active = 0;
3,428✔
445
    (*ppdecoder)->clipboard_output_active = 0;
3,428✔
446
    (*ppdecoder)->clipboard_input_format[0] = '\0';
3,428✔
447
    (*ppdecoder)->clipboard_output_format[0] = '\0';
3,428✔
448

449
    if ((*ppdecoder)->output == NULL || (*ppdecoder)->input == NULL) {
3,428!
450
        sixel_decoder_unref(*ppdecoder);
×
451
        *ppdecoder = NULL;
×
452
        sixel_helper_set_additional_message(
×
453
            "sixel_decoder_new: strdup_with_allocator() failed.");
454
        status = SIXEL_BAD_ALLOCATION;
×
455
        sixel_allocator_unref(allocator);
×
456
        goto end;
×
457
    }
458

459
    status = SIXEL_OK;
2,644✔
460

461
end:
1,680✔
462
    return status;
4,530✔
463
}
1,102✔
464

465

466
/* deprecated version of sixel_decoder_new() */
467
SIXELAPI /* deprecated */ sixel_decoder_t *
468
sixel_decoder_create(void)
×
469
{
470
    SIXELSTATUS status = SIXEL_FALSE;
×
471
    sixel_decoder_t *decoder = NULL;
×
472

473
    status = sixel_decoder_new(&decoder, NULL);
×
474
    if (SIXEL_FAILED(status)) {
×
475
        goto end;
476
    }
477

478
end:
479
    return decoder;
×
480
}
481

482

483
/* destroy a decoder object */
484
static void
485
sixel_decoder_destroy(sixel_decoder_t *decoder)
3,380✔
486
{
487
    sixel_allocator_t *allocator;
1,862✔
488

489
    if (decoder) {
3,380!
490
        allocator = decoder->allocator;
3,380✔
491
        sixel_allocator_free(allocator, decoder->input);
3,380✔
492
        sixel_allocator_free(allocator, decoder->output);
3,380✔
493
        sixel_allocator_free(allocator, decoder);
3,380✔
494
        sixel_allocator_unref(allocator);
3,380✔
495
    }
1,730✔
496
}
3,380✔
497

498

499
/* increase reference count of decoder object (thread-safe) */
500
SIXELAPI void
501
sixel_decoder_ref(sixel_decoder_t *decoder)
9,293✔
502
{
503
    if (decoder == NULL) {
9,293✔
504
        return;
505
    }
506

507
    (void)sixel_atomic_fetch_add_u32(&decoder->ref, 1U);
9,293✔
508
}
4,884✔
509

510

511
/* decrease reference count of decoder object (thread-safe) */
512
SIXELAPI void
513
sixel_decoder_unref(sixel_decoder_t *decoder)
12,674✔
514
{
515
    unsigned int previous;
7,028✔
516

517
    if (decoder == NULL) {
12,674✔
518
        return;
519
    }
520

521
    previous = sixel_atomic_fetch_sub_u32(&decoder->ref, 1U);
12,674✔
522
    if (previous == 1U) {
12,674✔
523
        sixel_decoder_destroy(decoder);
3,380✔
524
    }
1,730✔
525
}
6,614!
526

527

528
typedef struct sixel_similarity {
529
    const unsigned char *palette;
530
    int ncolors;
531
    int stride;
532
    signed char *cache;
533
    int bias;
534
} sixel_similarity_t;
535

536
static SIXELSTATUS
537
sixel_similarity_init(sixel_similarity_t *similarity,
204✔
538
                      const unsigned char *palette,
539
                      int ncolors,
540
                      int bias,
541
                      sixel_allocator_t *allocator)
542
{
543
    size_t cache_size;
104✔
544
    int i;
104✔
545

546
    if (bias < 1) {
204!
547
        bias = 1;
548
    }
549

550
    similarity->palette = palette;
204✔
551
    similarity->ncolors = ncolors;
204✔
552
    similarity->stride = ncolors;
204✔
553
    similarity->bias = bias;
204✔
554

555
    cache_size = (size_t)ncolors * (size_t)ncolors;
204✔
556
    if (cache_size == 0) {
204!
557
        similarity->cache = NULL;
×
558
        return SIXEL_OK;
×
559
    }
560

561
    similarity->cache = (signed char *)sixel_allocator_malloc(
204✔
562
        allocator,
84✔
563
        cache_size);
84✔
564
    if (similarity->cache == NULL) {
204!
565
        sixel_helper_set_additional_message(
×
566
            "sixel_similarity_init: sixel_allocator_malloc() failed.");
567
        return SIXEL_BAD_ALLOCATION;
×
568
    }
569
    memset(similarity->cache, -1, cache_size);
204✔
570
    for (i = 0; i < ncolors; ++i) {
19,788!
571
        similarity->cache[i * similarity->stride + i] = 7;
19,584✔
572
    }
9,264✔
573

574
    return SIXEL_OK;
148✔
575
}
84✔
576

577
static void
578
sixel_similarity_destroy(sixel_similarity_t *similarity,
204✔
579
                         sixel_allocator_t *allocator)
580
{
581
    if (similarity->cache != NULL) {
204!
582
        sixel_allocator_free(allocator, similarity->cache);
204✔
583
        similarity->cache = NULL;
204✔
584
    }
84✔
585
}
148✔
586

587
static inline unsigned int
588
sixel_similarity_diff(const unsigned char *a, const unsigned char *b)
7,836,960✔
589
{
590
    int dr = (int)a[0] - (int)b[0];
7,836,960✔
591
    int dg = (int)a[1] - (int)b[1];
7,836,960✔
592
    int db = (int)a[2] - (int)b[2];
7,836,960✔
593
    return (unsigned int)(dr * dr + dg * dg + db * db);
9,885,760✔
594
}
2,048,800✔
595

596
static unsigned int
597
sixel_similarity_compare(sixel_similarity_t *similarity,
5,098,860✔
598
                         int index1,
599
                         int index2,
600
                         int numerator,
601
                         int denominator)
602
{
603
    int min_index;
2,552,920✔
604
    int max_index;
2,552,920✔
605
    size_t cache_pos;
2,552,920✔
606
    signed char cached;
2,552,920✔
607
    const unsigned char *palette;
2,552,920✔
608
    const unsigned char *p1;
2,552,920✔
609
    const unsigned char *p2;
2,552,920✔
610
    unsigned char avg_color[3];
2,552,920✔
611
    unsigned int distance;
2,552,920✔
612
    unsigned int base_distance;
2,552,920✔
613
    unsigned long long scaled_distance;
2,552,920✔
614
    int bias;
2,552,920✔
615
    unsigned int min_diff = UINT_MAX;
5,098,860✔
616
    int i;
2,552,920✔
617
    unsigned int result;
2,552,920✔
618
    const unsigned char *pk;
2,552,920✔
619
    unsigned int diff;
2,552,920✔
620

621
    if (similarity->cache == NULL) {
5,098,860✔
622
        return 0;
623
    }
624

625
    if (index1 < 0 || index1 >= similarity->ncolors ||
6,812,440!
626
        index2 < 0 || index2 >= similarity->ncolors) {
5,098,860!
627
        return 0;
628
    }
629

630
    if (index1 <= index2) {
5,098,860✔
631
        min_index = index1;
2,465,404✔
632
        max_index = index2;
2,465,404✔
633
    } else {
1,311,252✔
634
        min_index = index2;
1,623,600✔
635
        max_index = index1;
1,623,600✔
636
    }
637

638
    cache_pos = (size_t)min_index * (size_t)similarity->stride
7,024,020✔
639
              + (size_t)max_index;
5,098,860✔
640
    cached = similarity->cache[cache_pos];
5,098,860✔
641
    if (cached >= 0) {
5,098,860✔
642
        return (unsigned int)cached;
5,060,364✔
643
    }
644

645
    palette = similarity->palette;
38,496✔
646
    p1 = palette + index1 * 3;
38,496✔
647
    p2 = palette + index2 * 3;
38,496✔
648

649
#if 1
650
   /*    original: n = (p1 + p2) / 2
651
    */
652
    avg_color[0] = (unsigned char)(((unsigned int)p1[0]
55,154✔
653
                                    + (unsigned int)p2[0]) >> 1);
38,496✔
654
    avg_color[1] = (unsigned char)(((unsigned int)p1[1]
55,154✔
655
                                    + (unsigned int)p2[1]) >> 1);
38,496✔
656
    avg_color[2] = (unsigned char)(((unsigned int)p1[2]
55,154✔
657
                                    + (unsigned int)p2[2]) >> 1);
38,496✔
658
    (void) numerator;
26,852✔
659
    (void) denominator;
26,852✔
660
#else
661
   /*
662
    *    diffuse(pos_a, n1) -> p1
663
    *    diffuse(pos_b, n2) -> p2
664
    *
665
    *    when n1 == n2 == n:
666
    *
667
    *    p2 = n + (n - p1) * numerator / denominator
668
    * => p2 * denominator = n * denominator + (n - p1) * numerator
669
    * => p2 * denominator = n * denominator + n * numerator - p1 * numerator
670
    * => n * (denominator + numerator) = p1 * numerator + p2 * denominator
671
    * => n = (p1 * numerator + p2 * denominator) / (denominator + numerator)
672
    *
673
    */
674
    avg_color[0] = (p1[0] * numerator + p2[0] * denominator + (numerator + denominator + 0.5) / 2)
675
                 / (numerator + denominator);
676
    avg_color[1] = (p1[1] * numerator + p2[1] * denominator + (numerator + denominator + 0.5) / 2)
677
                 / (numerator + denominator);
678
    avg_color[2] = (p1[2] * numerator + p2[2] * denominator + (numerator + denominator + 0.5) / 2)
679
                 / (numerator + denominator);
680
#endif
681

682
    distance = sixel_similarity_diff(avg_color, p1);
38,496✔
683
    bias = similarity->bias;
38,496✔
684
    if (bias < 1) {
38,496!
685
        bias = 1;
686
    }
687
    scaled_distance = (unsigned long long)distance
49,922✔
688
                    * (unsigned long long)bias
35,880✔
689
                    + 50ULL;
14,042✔
690
    base_distance = (unsigned int)(scaled_distance / 100ULL);
35,880✔
691
    if (base_distance == 0U) {
35,880!
692
        base_distance = 1U;
693
    }
694

695
    for (i = 0; i < similarity->ncolors; ++i) {
7,913,952!
696
        if (i == index1 || i == index2) {
7,875,456!
697
            continue;
76,992✔
698
        }
699
        pk = palette + i * 3;
7,798,464✔
700
        diff = sixel_similarity_diff(avg_color, pk);
7,798,464✔
701
        if (diff < min_diff) {
7,798,464✔
702
            min_diff = diff;
2,213,348✔
703
        }
115,232✔
704
    }
3,490,156✔
705

706
    if (min_diff == UINT_MAX) {
38,496!
707
        min_diff = base_distance * 2U;
×
708
    }
709

710
    if (min_diff >= base_distance * 2U) {
38,496✔
711
        result = 5U;
3,348✔
712
    } else if (min_diff >= base_distance) {
35,772!
713
        result = 8U;
3,532✔
714
    } else if ((unsigned long long)min_diff * 6ULL
42,624✔
715
               >= (unsigned long long)base_distance * 5ULL) {
28,932!
716
        result = 7U;
2,640✔
717
    } else if ((unsigned long long)min_diff * 4ULL
37,148✔
718
               >= (unsigned long long)base_distance * 3ULL) {
25,224!
719
        result = 7U;
694✔
720
    } else if ((unsigned long long)min_diff * 3ULL
34,806✔
721
               >= (unsigned long long)base_distance * 2ULL) {
24,264!
722
        result = 5U;
670✔
723
    } else if ((unsigned long long)min_diff * 5ULL
33,518✔
724
               >= (unsigned long long)base_distance * 3ULL) {
17,418!
725
        result = 7U;
800✔
726
    } else if ((unsigned long long)min_diff * 2ULL
32,080✔
727
               >= (unsigned long long)base_distance * 1ULL) {
16,618!
728
        result = 4U;
1,326✔
729
    } else if ((unsigned long long)min_diff * 3ULL
29,866✔
730
               >= (unsigned long long)base_distance * 1ULL) {
15,292!
731
        result = 2U;
2,718✔
732
    } else {
1,462✔
733
        result = 0U;
16,548✔
734
    }
735

736
    similarity->cache[cache_pos] = (signed char)result;
38,496✔
737

738
    return result;
38,496✔
739
}
1,925,160✔
740

741
static inline int
742
sixel_clamp(int value, int min_value, int max_value)
786,432✔
743
{
744
    if (value < min_value) {
786,432!
745
        return min_value;
4,608✔
746
    }
747
    if (value > max_value) {
781,824!
748
        return max_value;
4,608✔
749
    }
750
    return value;
777,216✔
751
}
786,432✔
752

753
static inline int
754
sixel_get_gray(const int *gray, int width, int height, int x, int y)
393,216✔
755
{
756
    int cx = sixel_clamp(x, 0, width - 1);
393,216✔
757
    int cy = sixel_clamp(y, 0, height - 1);
393,216!
758
    return gray[cy * width + cx];
655,360✔
759
}
262,144✔
760

761
static unsigned short
762
sixel_prewitt_value(const int *gray, int width, int height, int x, int y)
49,152✔
763
{
764
    int top_prev = sixel_get_gray(gray, width, height, x - 1, y - 1);
49,152!
765
    int top_curr = sixel_get_gray(gray, width, height, x, y - 1);
49,152!
766
    int top_next = sixel_get_gray(gray, width, height, x + 1, y - 1);
49,152!
767
    int mid_prev = sixel_get_gray(gray, width, height, x - 1, y);
49,152!
768
    int mid_next = sixel_get_gray(gray, width, height, x + 1, y);
49,152✔
769
    int bot_prev = sixel_get_gray(gray, width, height, x - 1, y + 1);
49,152!
770
    int bot_curr = sixel_get_gray(gray, width, height, x, y + 1);
49,152✔
771
    int bot_next = sixel_get_gray(gray, width, height, x + 1, y + 1);
49,152✔
772
    long gx = (long)top_next - (long)top_prev +
147,456✔
773
              (long)mid_next - (long)mid_prev +
147,456✔
774
              (long)bot_next - (long)bot_prev;
98,304✔
775
    long gy = (long)bot_prev + (long)bot_curr + (long)bot_next -
147,456✔
776
              (long)top_prev - (long)top_curr - (long)top_next;
98,304✔
777
    unsigned long long ux;
32,768✔
778
    unsigned long long uy;
32,768✔
779
    unsigned long long magnitude;
32,768✔
780

781
    /*
782
     * gx and gy are signed Prewitt gradients. Convert their absolute values
783
     * before squaring so unsigned-overflow sanitizers do not see the modulo
784
     * product that comes from casting a negative long directly to unsigned.
785
     */
786
    if (gx < 0L) {
49,152!
787
        ux = (unsigned long long)(-gx);
46,968✔
788
    } else {
46,968✔
789
        ux = (unsigned long long)gx;
2,184✔
790
    }
791
    if (gy < 0L) {
49,152!
792
        uy = (unsigned long long)(-gy);
49,140✔
793
    } else {
49,140✔
794
        uy = (unsigned long long)gy;
12✔
795
    }
796

797
    magnitude = ux * ux + uy * uy;
49,152✔
798
    magnitude /= 256ULL;
49,152✔
799
    if (magnitude > 65535ULL) {
49,152!
800
        magnitude = 65535ULL;
801
    }
802
    return (unsigned short)magnitude;
81,920✔
803
}
32,768✔
804

805
static unsigned short
806
sixel_scale_threshold(unsigned short base, int percent)
408✔
807
{
808
    unsigned long long numerator;
208✔
809
    unsigned long long scaled;
208✔
810

811
    if (percent <= 0) {
408!
812
        percent = 1;
272✔
813
    }
144✔
814

815
    numerator = (unsigned long long)base * 100ULL
592✔
816
              + (unsigned long long)percent / 2ULL;
360✔
817
    scaled = numerator / (unsigned long long)percent;
416✔
818
    if (scaled == 0ULL) {
416!
819
        scaled = 1ULL;
820
    }
821
    if (scaled > USHRT_MAX) {
384!
822
        scaled = USHRT_MAX;
823
    }
824

825
    return (unsigned short)scaled;
504✔
826
}
96✔
827

828
SIXEL_INTERNAL_API SIXELSTATUS
829
sixel_dequantize_k_undither(unsigned char *indexed_pixels,
204✔
830
                            int width,
831
                            int height,
832
                            unsigned char *palette,
833
                            int ncolors,
834
                            int similarity_bias,
835
                            int edge_strength,
836
                            sixel_allocator_t *allocator,
837
                            unsigned char **output)
838
{
839
    SIXELSTATUS status = SIXEL_FALSE;
204✔
840
    unsigned char *rgb = NULL;
204✔
841
    int *gray = NULL;
204✔
842
    unsigned short *prewitt = NULL;
204✔
843
    sixel_similarity_t similarity;
104✔
844
    size_t num_pixels;
104✔
845
    int x;
104✔
846
    int y;
104✔
847
    unsigned short strong_threshold;
104✔
848
    unsigned short detail_threshold;
104✔
849
    static const int neighbor_offsets[8][4] = {
56✔
850
        {-1, -1,  10, 16}, {0, -1, 16, 16}, {1, -1,   6, 16},
851
        {-1,  0,  11, 16},                  {1,  0,  11, 16},
852
        {-1,  1,   6, 16}, {0,  1, 16, 16}, {1,  1,  10, 16}
853
    };
854
    const unsigned char *color;
104✔
855
    size_t out_index;
104✔
856
    int palette_index;
104✔
857
    unsigned int center_weight;
104✔
858
    unsigned int total_weight = 0;
204✔
859
    unsigned int accum_r;
104✔
860
    unsigned int accum_g;
104✔
861
    unsigned int accum_b;
104✔
862
    unsigned short gradient;
104✔
863
    int neighbor;
104✔
864
    int nx;
104✔
865
    int ny;
104✔
866
    int numerator;
104✔
867
    int denominator;
104✔
868
    unsigned int weight;
104✔
869
    const unsigned char *neighbor_color;
104✔
870
    int neighbor_index;
104✔
871

872
    if (width <= 0 || height <= 0 || palette == NULL || ncolors <= 0) {
204!
873
        return SIXEL_BAD_INPUT;
874
    }
875

876
    num_pixels = (size_t)width * (size_t)height;
204✔
877

878
    memset(&similarity, 0, sizeof(sixel_similarity_t));
204!
879

880
    strong_threshold = sixel_scale_threshold(256U, edge_strength);
204!
881
    detail_threshold = sixel_scale_threshold(160U, edge_strength);
204!
882
    if (strong_threshold < detail_threshold) {
204!
883
        strong_threshold = detail_threshold;
884
    }
885

886
    /*
887
     * Build RGB and luminance buffers so we can reuse the similarity cache
888
     * and gradient analysis across the reconstructed image.
889
     */
890
    rgb = (unsigned char *)sixel_allocator_malloc(
204✔
891
        allocator,
84✔
892
        num_pixels * 3);
84✔
893
    if (rgb == NULL) {
204!
894
        sixel_helper_set_additional_message(
×
895
            "sixel_dequantize_k_undither: "
896
            "sixel_allocator_malloc() failed.");
897
        status = SIXEL_BAD_ALLOCATION;
×
898
        goto end;
×
899
    }
900

901
    gray = (int *)sixel_allocator_malloc(
244✔
902
        allocator,
84✔
903
        num_pixels * sizeof(int));
164✔
904
    if (gray == NULL) {
204!
905
        sixel_helper_set_additional_message(
×
906
            "sixel_dequantize_k_undither: "
907
            "sixel_allocator_malloc() failed.");
908
        status = SIXEL_BAD_ALLOCATION;
×
909
        goto end;
×
910
    }
911

912
    prewitt = (unsigned short *)sixel_allocator_malloc(
244✔
913
        allocator,
84✔
914
        num_pixels * sizeof(unsigned short));
164✔
915
    if (prewitt == NULL) {
204!
916
        sixel_helper_set_additional_message(
×
917
            "sixel_dequantize_k_undither: "
918
            "sixel_allocator_malloc() failed.");
919
        status = SIXEL_BAD_ALLOCATION;
×
920
        goto end;
×
921
    }
922

923
    /*
924
     * Pre-compute palette distance heuristics so each neighbour lookup reuses
925
     * the k_undither-style similarity table.
926
     */
927
    status = sixel_similarity_init(
204✔
928
        &similarity,
929
        palette,
84✔
930
        ncolors,
84✔
931
        similarity_bias,
84✔
932
        allocator);
84✔
933
    if (SIXEL_FAILED(status)) {
204!
934
        goto end;
×
935
    }
936

937
    for (y = 0; y < height; ++y) {
10,860!
938
        for (x = 0; x < width; ++x) {
712,128!
939
            palette_index = indexed_pixels[y * width + x];
701,472✔
940
            if (palette_index < 0 || palette_index >= ncolors) {
701,472!
941
                palette_index = 0;
×
942
            }
943

944
            color = palette + palette_index * 3;
701,472✔
945
            out_index = (size_t)(y * width + x) * 3;
701,472✔
946
            rgb[out_index + 0] = color[0];
701,472✔
947
            rgb[out_index + 1] = color[1];
701,472✔
948
            rgb[out_index + 2] = color[2];
701,472✔
949

950
            if (edge_strength > 0) {
701,472!
951
                gray[y * width + x] = (int)color[0]
98,304✔
952
                                    + (int)color[1] * 2
49,152✔
953
                                    + (int)color[2];
49,152✔
954
                /*
955
                 * Edge detection keeps high-frequency content intact while we
956
                 * smooth dithering noise in flatter regions.
957
                 */
958
                prewitt[y * width + x] = sixel_prewitt_value(
49,152✔
959
                    gray,
49,152✔
960
                    width,
49,152✔
961
                    height,
49,152✔
962
                    x,
49,152✔
963
                    y);
49,152✔
964

965
                gradient = prewitt[y * width + x];
49,152✔
966
                if (gradient > strong_threshold) {
49,152!
967
                    continue;
46,500✔
968
                }
969

970
                if (gradient > detail_threshold) {
2,652!
971
                    center_weight = 24U;
1,908✔
972
                } else {
1,908✔
973
                    center_weight = 8U;
191,004✔
974
                }
975
            } else {
2,652✔
976
                center_weight = 8U;
462,060✔
977
            }
978

979
            out_index = (size_t)(y * width + x) * 3;
654,972✔
980
            accum_r = (unsigned int)rgb[out_index + 0] * center_weight;
654,972✔
981
            accum_g = (unsigned int)rgb[out_index + 1] * center_weight;
654,972✔
982
            accum_b = (unsigned int)rgb[out_index + 2] * center_weight;
654,972✔
983
            total_weight = center_weight;
654,972✔
984

985
            /*
986
             * Blend neighbours that stay within the palette similarity
987
             * threshold so Floyd-Steinberg noise is averaged away without
988
             * bleeding across pronounced edges.
989
             */
990
            for (neighbor = 0; neighbor < 8; ++neighbor) {
5,894,748!
991
                nx = x + neighbor_offsets[neighbor][0];
5,239,776✔
992
                ny = y + neighbor_offsets[neighbor][1];
5,239,776✔
993
                numerator = neighbor_offsets[neighbor][2];
5,239,776✔
994
                denominator = neighbor_offsets[neighbor][3];
5,239,776✔
995

996
                if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
5,239,776!
997
                    continue;
140,916✔
998
                }
999

1000
                neighbor_index = indexed_pixels[ny * width + nx];
5,098,860✔
1001
                if (neighbor_index < 0 || neighbor_index >= ncolors) {
5,098,860!
1002
                    continue;
×
1003
                }
1004

1005
                if (numerator) {
5,098,860!
1006
                    weight = sixel_similarity_compare(
5,098,860✔
1007
                        &similarity,
1008
                        palette_index,
1,925,160✔
1009
                        neighbor_index,
1,925,160✔
1010
                        numerator,
1,925,160✔
1011
                        denominator);
1,925,160✔
1012
                    if (weight == 0) {
5,098,860✔
1013
                        continue;
191,544✔
1014
                    }
1015

1016
                    neighbor_color = palette + neighbor_index * 3;
4,907,316✔
1017
                    accum_r += (unsigned int)neighbor_color[0] * weight;
4,907,316✔
1018
                    accum_g += (unsigned int)neighbor_color[1] * weight;
4,907,316✔
1019
                    accum_b += (unsigned int)neighbor_color[2] * weight;
4,907,316✔
1020
                    total_weight += weight;
4,907,316✔
1021
                }
1,844,692✔
1022
            }
1,844,692✔
1023

1024
            if (total_weight > 0U) {
654,972!
1025
                rgb[out_index + 0] = (unsigned char)(accum_r / total_weight);
654,972✔
1026
                rgb[out_index + 1] = (unsigned char)(accum_g / total_weight);
654,972✔
1027
                rgb[out_index + 2] = (unsigned char)(accum_b / total_weight);
654,972✔
1028
            }
247,272✔
1029
        }
247,272✔
1030
    }
4,476✔
1031

1032

1033
    *output = rgb;
204✔
1034
    rgb = NULL;
204✔
1035
    status = SIXEL_OK;
204✔
1036

1037
end:
120✔
1038
    sixel_similarity_destroy(&similarity, allocator);
204!
1039
    sixel_allocator_free(allocator, rgb);
204✔
1040
    sixel_allocator_free(allocator, gray);
204✔
1041
    sixel_allocator_free(allocator, prewitt);
204✔
1042
    return status;
204✔
1043
}
84✔
1044
/*
1045
 * The dequantizer accepts a method supplied by the shared option helper. The
1046
 * decoder keeps a parallel lookup table that translates the matched index
1047
 * into the execution constant.
1048
 */
1049
static int const g_decoder_dequant_methods[] = {
1050
    SIXEL_DEQUANTIZE_NONE,
1051
    SIXEL_DEQUANTIZE_K_UNDITHER
1052
};
1053

1054
static sixel_option_choice_t const g_decoder_dequant_choices[] = {
1055
    { "none", 0 },
1056
    { "k_undither", 1 }
1057
};
1058

1059
/* set an option flag to decoder object */
1060
SIXELAPI SIXELSTATUS
1061
sixel_decoder_setopt(
6,010✔
1062
    sixel_decoder_t /* in */ *decoder,
1063
    int             /* in */ arg,
1064
    char const      /* in */ *value
1065
)
1066
{
1067
    SIXELSTATUS status = SIXEL_FALSE;
6,010✔
1068
    unsigned int path_flags;
3,352✔
1069
    int path_check;
3,352✔
1070
    char const *payload = NULL;
6,010✔
1071
    sixel_clipboard_spec_t clipboard_spec;
3,352✔
1072
    int match_index;
3,352✔
1073
    sixel_option_choice_result_t match_result;
3,352✔
1074
    char match_detail[128];
3,352✔
1075
    char match_message[256];
3,352✔
1076
    char const *filename = NULL;
6,010✔
1077
    size_t libc_buffer_size;
3,352✔
1078
    char *libc_buffer;
3,352✔
1079
    char const *libc_path;
3,352✔
1080
    long bias;
3,352✔
1081
    long parsed_value;
3,352✔
1082
    char *endptr;
3,352✔
1083

1084
    sixel_decoder_ref(decoder);
6,010✔
1085
    path_flags = 0u;
6,010✔
1086
    path_check = 0;
6,010✔
1087
    libc_buffer_size = 0u;
6,010✔
1088
    libc_buffer = NULL;
6,010✔
1089
    libc_path = NULL;
6,010✔
1090

1091
    switch(arg) {
6,010!
1092
    case SIXEL_OPTFLAG_INPUT:  /* i */
1,260✔
1093
        path_flags = SIXEL_OPTION_PATH_ALLOW_STDIN |
2,756✔
1094
            SIXEL_OPTION_PATH_ALLOW_CLIPBOARD |
1095
            SIXEL_OPTION_PATH_ALLOW_REMOTE;
1096
        if (value != NULL) {
2,756!
1097
            path_check = sixel_option_validate_filesystem_path(
2,756✔
1098
                value,
1,496✔
1099
                value,
1,496✔
1100
                path_flags);
1,496✔
1101
            if (path_check != 0) {
2,756!
1102
                status = SIXEL_BAD_ARGUMENT;
×
1103
                goto end;
×
1104
            }
1105
        }
1,496✔
1106
        decoder->clipboard_input_active = 0;
2,756✔
1107
        decoder->clipboard_input_format[0] = '\0';
2,756✔
1108
        if (value != NULL) {
2,756!
1109
            clipboard_spec.is_clipboard = 0;
2,756✔
1110
            clipboard_spec.format[0] = '\0';
2,756✔
1111
            if (sixel_clipboard_parse_spec(value, &clipboard_spec)
2,756!
1112
                    && clipboard_spec.is_clipboard) {
1,542!
1113
                decoder_clipboard_select_format(
82✔
1114
                    decoder->clipboard_input_format,
82✔
1115
                    sizeof(decoder->clipboard_input_format),
1116
                    clipboard_spec.format,
36✔
1117
                    "sixel");
1118
                decoder->clipboard_input_active = 1;
82✔
1119
            }
36✔
1120
        }
1,496✔
1121
        free(decoder->input);
2,756✔
1122
        decoder->input = strdup_with_allocator(value, decoder->allocator);
2,756✔
1123
        if (decoder->input == NULL) {
2,756!
1124
            sixel_helper_set_additional_message(
×
1125
                "sixel_decoder_setopt: strdup_with_allocator() failed.");
1126
            status = SIXEL_BAD_ALLOCATION;
×
1127
            goto end;
×
1128
        }
1129
        break;
2,168✔
1130
    case SIXEL_OPTFLAG_OUTPUT:  /* o */
1,260✔
1131
        decoder->clipboard_output_active = 0;
2,756✔
1132
        decoder->clipboard_output_format[0] = '\0';
2,756✔
1133

1134
        payload = value;
2,756✔
1135
        if (strncmp(value, "png:", 4) == 0) {
2,756✔
1136
            payload = value + 4;
240✔
1137
            if (payload[0] == '\0') {
240✔
1138
                sixel_helper_set_additional_message(
48✔
1139
                    "missing target after the \"png:\" prefix.");
1140
                return SIXEL_BAD_ARGUMENT;
48✔
1141
            }
1142
            libc_buffer_size = sixel_path_to_libc_buffer_size(payload);
192✔
1143
            if (libc_buffer_size > 0u) {
192!
1144
                libc_buffer = (char *)malloc(libc_buffer_size);
12✔
1145
                if (libc_buffer == NULL) {
12!
1146
                    sixel_helper_set_additional_message(
×
1147
                        "sixel_decoder_setopt: malloc() failed for png path "
1148
                        "buffer.");
1149
                    return SIXEL_BAD_ALLOCATION;
×
1150
                }
1151
                libc_path = sixel_path_to_libc(payload,
12✔
1152
                                               libc_buffer,
1153
                                               libc_buffer_size);
1154
                if (libc_path == NULL) {
12!
1155
                    sixel_helper_set_additional_message(
×
1156
                        "sixel_decoder_setopt: invalid png output path.");
1157
                    free(libc_buffer);
×
1158
                    return SIXEL_BAD_ARGUMENT;
×
1159
                }
1160
                filename = libc_path;
2✔
1161
            } else {
1162
                filename = payload;
134✔
1163
            }
1164
        } else {
72✔
1165
            filename = value;
1,998✔
1166
        }
1167

1168
        if (filename != NULL) {
2,134!
1169
            clipboard_spec.is_clipboard = 0;
2,708✔
1170
            clipboard_spec.format[0] = '\0';
2,708✔
1171
            if (sixel_clipboard_parse_spec(filename, &clipboard_spec)
2,708!
1172
                    && clipboard_spec.is_clipboard) {
1,538!
1173
                decoder_clipboard_select_format(
96✔
1174
                    decoder->clipboard_output_format,
96✔
1175
                    sizeof(decoder->clipboard_output_format),
1176
                    clipboard_spec.format,
36✔
1177
                    "png");
1178
                decoder->clipboard_output_active = 1;
96✔
1179
            }
36✔
1180
        }
1,478✔
1181
        free(decoder->output);
2,794✔
1182
        decoder->output = strdup_with_allocator(filename, decoder->allocator);
2,794✔
1183
        if (libc_buffer != NULL) {
2,794!
1184
            free(libc_buffer);
12✔
1185
            libc_buffer = NULL;
12✔
1186
        }
1187
        if (decoder->output == NULL) {
2,708!
1188
            sixel_helper_set_additional_message(
×
1189
                "sixel_decoder_setopt: strdup_with_allocator() failed.");
1190
            status = SIXEL_BAD_ALLOCATION;
×
1191
            goto end;
×
1192
        }
1193
        break;
2,134✔
1194
    case SIXEL_OPTFLAG_DEQUANTIZE:  /* d */
60✔
1195
        if (value == NULL) {
102!
1196
            sixel_helper_set_additional_message(
×
1197
                "sixel_decoder_setopt: -d/--dequantize requires an argument.");
1198
            status = SIXEL_BAD_ALLOCATION;
×
1199
            goto end;
×
1200
        }
1201

1202
        match_index = 0;
102✔
1203
        memset(match_detail, 0, sizeof(match_detail));
102✔
1204
        memset(match_message, 0, sizeof(match_message));
102✔
1205

1206
        match_result = sixel_option_match_choice(
102✔
1207
            value,
42✔
1208
            g_decoder_dequant_choices,
1209
            sizeof(g_decoder_dequant_choices) /
1210
            sizeof(g_decoder_dequant_choices[0]),
1211
            &match_index,
1212
            match_detail,
42✔
1213
            sizeof(match_detail));
1214
        if (match_result == SIXEL_OPTION_CHOICE_MATCH) {
102!
1215
            decoder->dequantize_method =
102✔
1216
                g_decoder_dequant_methods[match_index];
102✔
1217
        } else {
42✔
1218
            if (match_result == SIXEL_OPTION_CHOICE_AMBIGUOUS) {
×
1219
                sixel_option_report_ambiguous_prefix(
×
1220
                    value,
1221
                    match_detail,
1222
                    match_message,
1223
                    sizeof(match_message));
1224
            } else {
1225
                sixel_option_report_invalid_choice(
×
1226
                    "unsupported dequantize method.",
1227
                    match_detail,
1228
                    match_message,
1229
                    sizeof(match_message));
1230
            }
1231
            status = SIXEL_BAD_ARGUMENT;
×
1232
            goto end;
×
1233
        }
1234
        break;
102✔
1235

1236
    case SIXEL_OPTFLAG_SIMILARITY:  /* S */
30✔
1237
        errno = 0;
54✔
1238
        bias = strtol(value, &endptr, 10);
54✔
1239
        if (endptr == value || endptr[0] != '\0' ||
60!
1240
            errno == ERANGE || bias < 0 || bias > 1000) {
6!
1241
            sixel_helper_set_additional_message(
48✔
1242
                "similarity must be an integer between 0 and 1000.");
1243
            status = SIXEL_BAD_ARGUMENT;
48✔
1244
            goto end;
48✔
1245
        }
1246

1247
        decoder->dequantize_similarity_bias = (int)bias;
6✔
1248
        break;
6✔
1249

1250
    case SIXEL_OPTFLAG_SIZE:  /* s */
1251
        parsed_value = 0L;
×
1252
        endptr = NULL;
×
1253
        errno = 0;
×
1254
        parsed_value = strtol(value, &endptr, 10);
×
1255
        if (endptr == value || *endptr != '\0' || errno == ERANGE ||
×
1256
            parsed_value < 1L || parsed_value > (long)INT_MAX) {
×
1257
            sixel_helper_set_additional_message(
×
1258
                "size must be greater than zero.");
1259
            status = SIXEL_BAD_ARGUMENT;
×
1260
            goto end;
×
1261
        }
1262
        decoder->thumbnail_size = (int)parsed_value;
×
1263
        break;
×
1264

1265
    case SIXEL_OPTFLAG_EDGE:  /* e */
1266
        parsed_value = 0L;
6✔
1267
        endptr = NULL;
6✔
1268
        errno = 0;
6✔
1269
        parsed_value = strtol(value, &endptr, 10);
6✔
1270
        if (endptr == value || *endptr != '\0' || errno == ERANGE ||
12!
1271
            parsed_value < 0L || parsed_value > 1000L) {
6!
1272
            sixel_helper_set_additional_message(
×
1273
                "edge bias must be between 1 and 1000.");
1274
            status = SIXEL_BAD_ARGUMENT;
×
1275
            goto end;
×
1276
        }
1277
        decoder->dequantize_edge_strength = (int)parsed_value;
6✔
1278
        break;
6✔
1279

1280
    case SIXEL_OPTFLAG_DIRECT:  /* D */
180✔
1281
        decoder->direct_color = 1;
288✔
1282
        break;
288✔
1283

1284
    case SIXEL_OPTFLAG_THREADS:  /* = */
30✔
1285
        status = sixel_decoder_parallel_override_threads(value);
48✔
1286
        if (SIXEL_FAILED(status)) {
48!
1287
            goto end;
48✔
1288
        }
1289
        break;
1290

1291
    case '?':
×
1292
    default:
1293
        status = SIXEL_BAD_ARGUMENT;
×
1294
        goto end;
×
1295
    }
1296

1297
    status = SIXEL_OK;
4,592✔
1298

1299
end:
2,790✔
1300
    sixel_decoder_unref(decoder);
5,962✔
1301

1302
    return status;
5,962✔
1303
}
3,190✔
1304

1305

1306
/* load source data from stdin or the file specified with
1307
   SIXEL_OPTFLAG_INPUT flag, and decode it */
1308
SIXELAPI SIXELSTATUS
1309
sixel_decoder_decode(
3,284✔
1310
    sixel_decoder_t /* in */ *decoder)
1311
{
1312
    SIXELSTATUS status = SIXEL_FALSE;
3,284✔
1313
    unsigned char *raw_data = NULL;
3,284✔
1314
    int sx;
1,814✔
1315
    int sy;
1,814✔
1316
    int raw_len;
1,814✔
1317
    int max;
1,814✔
1318
    int n;
1,814✔
1319
    FILE *input_fp = NULL;
3,284✔
1320
    char message[2048];
1,814✔
1321
    unsigned char *indexed_pixels = NULL;
3,284✔
1322
    unsigned char *palette = NULL;
3,284✔
1323
    unsigned char *rgb_pixels = NULL;
3,284✔
1324
    unsigned char *direct_pixels = NULL;
3,284✔
1325
    unsigned char *output_pixels;
1,814✔
1326
    unsigned char *output_palette;
1,814✔
1327
    int output_pixelformat;
1,814✔
1328
    int ncolors;
1,814✔
1329
    sixel_frame_t *frame;
1,814✔
1330
    int new_width;
1,814✔
1331
    int new_height;
1,814✔
1332
    double scaled_width;
1,814✔
1333
    double scaled_height;
1,814✔
1334
    int max_dimension;
1,814✔
1335
    int thumbnail_size;
1,814✔
1336
    int frame_ncolors;
1,814✔
1337
    unsigned char *clipboard_blob;
1,814✔
1338
    size_t clipboard_blob_size;
1,814✔
1339
    SIXELSTATUS clipboard_status;
1,814✔
1340
    char *clipboard_output_path;
1,814✔
1341
    unsigned char *clipboard_output_data;
1,814✔
1342
    size_t clipboard_output_size;
1,814✔
1343
    SIXELSTATUS clipboard_output_status;
1,814✔
1344
    sixel_timeline_logger_t *logger;
1,814✔
1345
    int logger_prepared;
1,814✔
1346

1347
    sixel_decoder_ref(decoder);
3,284✔
1348

1349
    frame = NULL;
3,284✔
1350
    new_width = 0;
3,284✔
1351
    new_height = 0;
3,284✔
1352
    scaled_width = 0.0;
3,284✔
1353
    scaled_height = 0.0;
3,284✔
1354
    max_dimension = 0;
3,284✔
1355
    thumbnail_size = decoder->thumbnail_size;
3,284✔
1356
    frame_ncolors = -1;
3,284✔
1357
    clipboard_blob = NULL;
3,284✔
1358
    clipboard_blob_size = 0u;
3,284✔
1359
    clipboard_status = SIXEL_OK;
3,284✔
1360
    clipboard_output_path = NULL;
3,284✔
1361
    clipboard_output_data = NULL;
3,284✔
1362
    clipboard_output_size = 0u;
3,284✔
1363
    clipboard_output_status = SIXEL_OK;
3,284✔
1364
    input_fp = NULL;
3,284✔
1365
    logger = NULL;
3,284✔
1366
    (void)sixel_timeline_logger_prepare_env(decoder->allocator, &logger);
3,284✔
1367
    logger_prepared = logger != NULL;
3,284✔
1368

1369
    raw_len = 0;
3,284✔
1370
    max = 0;
3,284✔
1371
    if (decoder->clipboard_input_active) {
3,284✔
1372
        clipboard_status = sixel_clipboard_read(
104✔
1373
            decoder->clipboard_input_format,
82✔
1374
            &clipboard_blob,
1375
            &clipboard_blob_size,
1376
            decoder->allocator);
36✔
1377
        if (SIXEL_FAILED(clipboard_status)) {
82!
1378
            status = clipboard_status;
×
1379
            goto end;
×
1380
        }
1381
        max = (int)((clipboard_blob_size > 0u)
82!
1382
                    ? clipboard_blob_size
36✔
1383
                    : 1u);
1384
        raw_data = (unsigned char *)sixel_allocator_malloc(
82✔
1385
            decoder->allocator,
36✔
1386
            (size_t)max);
36✔
1387
        if (raw_data == NULL) {
82!
1388
            sixel_helper_set_additional_message(
×
1389
                "sixel_decoder_decode: sixel_allocator_malloc() failed.");
1390
            status = SIXEL_BAD_ALLOCATION;
×
1391
            goto end;
×
1392
        }
1393
        if (clipboard_blob_size > 0u && clipboard_blob != NULL) {
82!
1394
            memcpy(raw_data, clipboard_blob, clipboard_blob_size);
82✔
1395
        }
36✔
1396
        raw_len = (int)clipboard_blob_size;
82✔
1397
        if (clipboard_blob != NULL) {
82!
1398
            free(clipboard_blob);
82✔
1399
            clipboard_blob = NULL;
82✔
1400
        }
36✔
1401
    } else {
36✔
1402
        if (strcmp(decoder->input, "-") == 0) {
3,202✔
1403
            /* for windows */
1404
#if defined(O_BINARY)
1405
            (void)sixel_compat_set_binary(STDIN_FILENO);
330✔
1406
#endif  /* defined(O_BINARY) */
1407
            input_fp = stdin;
720✔
1408
        } else {
270✔
1409
            input_fp = sixel_compat_fopen(decoder->input, "rb");
2,482✔
1410
            if (! input_fp) {
2,482✔
1411
                (void)snprintf(
48✔
1412
                    message,
2✔
1413
                    sizeof(message) - 1,
1414
                    "sixel_decoder_decode: failed to open input file: %s.",
1415
                    decoder->input);
2✔
1416
                sixel_helper_set_additional_message(message);
48✔
1417
                status = (SIXEL_LIBC_ERROR | (errno & 0xff));
48✔
1418
                goto end;
48✔
1419
            }
1420
        }
1421

1422
        raw_len = 0;
3,154✔
1423
        max = 64 * 1024;
3,154✔
1424

1425
        raw_data = (unsigned char *)sixel_allocator_malloc(
3,154✔
1426
            decoder->allocator,
1,640✔
1427
            (size_t)max);
1,640✔
1428
        if (raw_data == NULL) {
3,154!
1429
            sixel_helper_set_additional_message(
×
1430
                "sixel_decoder_decode: sixel_allocator_malloc() failed.");
1431
            status = SIXEL_BAD_ALLOCATION;
×
1432
            goto end;
×
1433
        }
1434

1435
        for (;;) {
9,024✔
1436
            if ((max - raw_len) < 4096) {
9,974!
1437
                max *= 2;
96✔
1438
                raw_data = (unsigned char *)sixel_allocator_realloc(
96✔
1439
                    decoder->allocator,
96✔
1440
                    raw_data,
96✔
1441
                    (size_t)max);
96✔
1442
                if (raw_data == NULL) {
96!
1443
                    sixel_helper_set_additional_message(
×
1444
                        "sixel_decoder_decode: sixel_allocator_realloc() failed.");
1445
                    status = SIXEL_BAD_ALLOCATION;
×
1446
                    goto end;
×
1447
                }
1448
            }
96✔
1449
            if ((n = (int)fread(raw_data + raw_len, 1, 4096, input_fp)) <= 0) {
10,454!
1450
                break;
2,448✔
1451
            }
1452
            raw_len += n;
6,820✔
1453
        }
1454

1455
        if (input_fp != NULL && input_fp != stdin) {
3,154!
1456
            fclose(input_fp);
2,434✔
1457
        }
1,370✔
1458
    }
1459

1460
    if (decoder->direct_color != 0 &&
3,236!
1461
            decoder->dequantize_method != SIXEL_DEQUANTIZE_NONE) {
288✔
1462
        sixel_helper_set_additional_message(
48✔
1463
            "sixel_decoder_decode: direct option "
1464
            "cannot be combined with dequantize option.");
1465
        status = SIXEL_BAD_ARGUMENT;
48✔
1466
        goto end;
48✔
1467
    }
1468

1469
    ncolors = 0;
3,188✔
1470

1471
    if (decoder->direct_color != 0) {
3,188✔
1472
        status = sixel_decode_direct(
240✔
1473
            raw_data,
90✔
1474
            raw_len,
90✔
1475
            &direct_pixels,
1476
            &sx,
1477
            &sy,
1478
            decoder->allocator);
90✔
1479
    } else {
90✔
1480
        status = sixel_decode_raw(
2,948✔
1481
            raw_data,
1,568✔
1482
            raw_len,
1,568✔
1483
            &indexed_pixels,
1484
            &sx,
1485
            &sy,
1486
            &palette,
1487
            &ncolors,
1488
            decoder->allocator);
1,568✔
1489
    }
1490
    if (SIXEL_FAILED(status)) {
3,188✔
1491
        goto end;
48✔
1492
    }
1493

1494
    if (sx > SIXEL_WIDTH_LIMIT || sy > SIXEL_HEIGHT_LIMIT) {
3,140!
1495
        status = SIXEL_BAD_INPUT;
×
1496
        goto end;
×
1497
    }
1498

1499
    if (decoder->direct_color != 0) {
3,140✔
1500
        output_pixels = direct_pixels;
240✔
1501
        output_palette = NULL;
240✔
1502
        output_pixelformat = SIXEL_PIXELFORMAT_RGBA8888;
240✔
1503
        frame_ncolors = 0;
240✔
1504
    } else {
90✔
1505
        output_pixels = indexed_pixels;
2,900✔
1506
        output_palette = palette;
2,900✔
1507
        output_pixelformat = SIXEL_PIXELFORMAT_PAL8;
2,900✔
1508

1509
        if (decoder->dequantize_method == SIXEL_DEQUANTIZE_K_UNDITHER) {
2,900✔
1510
            if (logger_prepared) {
54!
1511
                sixel_timeline_logger_logf(logger,
×
1512
                                  "decoder",
1513
                                  "undither",
1514
                                  "start",
1515
                                  0);
1516
            }
1517
            status = sixel_dequantize_k_undither(
54✔
1518
                indexed_pixels,
24✔
1519
                sx,
24✔
1520
                sy,
24✔
1521
                palette,
24✔
1522
                ncolors,
24✔
1523
                decoder->dequantize_similarity_bias,
24✔
1524
                decoder->dequantize_edge_strength,
24✔
1525
                decoder->allocator,
24✔
1526
                &rgb_pixels);
1527
            if (SIXEL_FAILED(status)) {
54!
1528
                if (logger_prepared) {
×
1529
                    sixel_timeline_logger_logf(
×
1530
                        logger,
1531
                        "decoder",
1532
                        "undither",
1533
                        "abort",
1534
                        0);
1535
                }
1536
                goto end;
×
1537
            }
1538
            if (logger_prepared) {
54!
1539
                sixel_timeline_logger_logf(logger,
×
1540
                                  "decoder",
1541
                                  "undither",
1542
                                  "finish",
1543
                                  0);
1544
            }
1545
            output_pixels = rgb_pixels;
54✔
1546
            output_palette = NULL;
54✔
1547
            output_pixelformat = SIXEL_PIXELFORMAT_RGB888;
54✔
1548
        }
24✔
1549

1550
        if (output_pixelformat == SIXEL_PIXELFORMAT_PAL8) {
2,284✔
1551
            frame_ncolors = ncolors;
2,846✔
1552
        } else {
1,526✔
1553
            frame_ncolors = 0;
40✔
1554
        }
1555
    }
1556

1557
    if (thumbnail_size > 0) {
3,140!
1558
        /*
1559
         * When the caller requests a thumbnail, compute the new geometry
1560
         * while preserving the original aspect ratio. We only allocate a
1561
         * frame when the dimensions actually change, so the fast path for
1562
         * matching sizes still avoids any additional allocations.
1563
         */
1564
        max_dimension = sx;
×
1565
        if (sy > max_dimension) {
×
1566
            max_dimension = sy;
1567
        }
1568
        if (max_dimension > 0) {
×
1569
            if (sx >= sy) {
×
1570
                new_width = thumbnail_size;
×
1571
                scaled_height = (double)sy * (double)thumbnail_size /
×
1572
                    (double)sx;
×
1573
                new_height = (int)(scaled_height + 0.5);
×
1574
            } else {
1575
                new_height = thumbnail_size;
×
1576
                scaled_width = (double)sx * (double)thumbnail_size /
×
1577
                    (double)sy;
×
1578
                new_width = (int)(scaled_width + 0.5);
×
1579
            }
1580
            if (new_width < 1) {
×
1581
                new_width = 1;
1582
            }
1583
            if (new_height < 1) {
×
1584
                new_height = 1;
1585
            }
1586
            if (new_width != sx || new_height != sy) {
×
1587
                /*
1588
                 * Wrap the decoded pixels in a frame so we can reuse the
1589
                 * central scaling helper. Ownership transfers to the frame,
1590
                 * which keeps the lifetime rules identical on both paths.
1591
                 */
1592
                status = sixel_frame_create_from_factory(
×
1593
                    &frame,
1594
                    decoder->allocator);
1595
                if (SIXEL_FAILED(status)) {
×
1596
                    goto end;
×
1597
                }
1598
                status = sixel_frame_init(
×
1599
                    frame,
1600
                    output_pixels,
1601
                    sx,
1602
                    sy,
1603
                    output_pixelformat,
1604
                    output_palette,
1605
                    frame_ncolors);
1606
                if (SIXEL_FAILED(status)) {
×
1607
                    goto end;
×
1608
                }
1609
                if (output_pixels == indexed_pixels) {
×
1610
                    indexed_pixels = NULL;
×
1611
                }
1612
                if (output_pixels == rgb_pixels) {
×
1613
                    rgb_pixels = NULL;
×
1614
                }
1615
                if (output_palette == palette) {
×
1616
                    palette = NULL;
×
1617
                }
1618
                status = sixel_frame_resize(
×
1619
                    frame,
1620
                    new_width,
1621
                    new_height,
1622
                    SIXEL_RES_BILINEAR);
1623
                if (SIXEL_FAILED(status)) {
×
1624
                    goto end;
×
1625
                }
1626
                /*
1627
                 * The resized frame already exposes a tightly packed RGB
1628
                 * buffer, so write the updated dimensions and references
1629
                 * back to the main encoder path.
1630
                 */
1631
                sx = sixel_frame_get_width(frame);
×
1632
                sy = sixel_frame_get_height(frame);
×
1633
                output_pixels = sixel_frame_get_pixels(frame);
×
1634
                output_palette = NULL;
×
1635
                output_pixelformat = sixel_frame_get_pixelformat(frame);
×
1636
            }
1637
        }
1638
    }
1639

1640
    if (decoder->clipboard_output_active) {
3,040✔
1641
        clipboard_output_status = decoder_clipboard_create_spool(
96✔
1642
            decoder->allocator,
36✔
1643
            "clipboard-out",
1644
            &clipboard_output_path);
1645
        if (SIXEL_FAILED(clipboard_output_status)) {
96!
1646
            status = clipboard_output_status;
×
1647
            goto end;
×
1648
        }
1649
    }
36✔
1650

1651
    if (logger_prepared) {
3,040✔
1652
        sixel_timeline_logger_logf(logger,
80✔
1653
                          "io",
1654
                          "png",
1655
                          "start",
1656
                          0);
1657
    }
36✔
1658
    status = sixel_helper_write_image_file(
3,140✔
1659
        output_pixels,
1,640✔
1660
        sx,
1,640✔
1661
        sy,
1,640✔
1662
        output_palette,
1,640✔
1663
        output_pixelformat,
1,640✔
1664
        decoder->clipboard_output_active
3,140✔
1665
            ? clipboard_output_path
36✔
1666
            : decoder->output,
1,604✔
1667
        SIXEL_FORMAT_PNG,
1668
        decoder->allocator);
1,640✔
1669
    if (SIXEL_FAILED(status)) {
3,140!
1670
        if (logger_prepared) {
×
1671
            sixel_timeline_logger_logf(logger,
×
1672
                              "io",
1673
                              "png",
1674
                              "abort",
1675
                              0);
1676
        }
1677
        goto end;
×
1678
    }
1679
    if (logger_prepared) {
3,140✔
1680
        sixel_timeline_logger_logf(logger,
80✔
1681
                          "io",
1682
                          "png",
1683
                          "finish",
1684
                          0);
1685
    }
36✔
1686

1687
    if (decoder->clipboard_output_active) {
3,076✔
1688
        clipboard_output_status = decoder_clipboard_read_file(
96✔
1689
            clipboard_output_path,
36✔
1690
            &clipboard_output_data,
1691
            &clipboard_output_size);
1692
        if (SIXEL_SUCCEEDED(clipboard_output_status)) {
96!
1693
            clipboard_output_status = sixel_clipboard_write(
96✔
1694
                decoder->clipboard_output_format,
96✔
1695
                clipboard_output_data,
36✔
1696
                clipboard_output_size);
36✔
1697
        }
36✔
1698
        if (clipboard_output_data != NULL) {
96!
1699
            free(clipboard_output_data);
96✔
1700
            clipboard_output_data = NULL;
96✔
1701
        }
36✔
1702
        if (SIXEL_FAILED(clipboard_output_status)) {
96!
1703
            status = clipboard_output_status;
14✔
1704
            goto end;
14✔
1705
        }
1706
    }
36✔
1707

1708
end:
1,486✔
1709
    sixel_frame_unref(frame);
3,284✔
1710
    sixel_allocator_free(decoder->allocator, raw_data);
3,284✔
1711
    sixel_allocator_free(decoder->allocator, indexed_pixels);
3,284✔
1712
    sixel_allocator_free(decoder->allocator, palette);
3,284✔
1713
    sixel_allocator_free(decoder->allocator, direct_pixels);
3,284✔
1714
    sixel_allocator_free(decoder->allocator, rgb_pixels);
3,284✔
1715
    if (clipboard_blob != NULL) {
3,284!
1716
        free(clipboard_blob);
×
1717
    }
1718
    if (clipboard_output_path != NULL) {
3,178✔
1719
        (void)sixel_compat_unlink(clipboard_output_path);
96✔
1720
        sixel_allocator_free(decoder->allocator, clipboard_output_path);
96✔
1721
    }
36✔
1722

1723
    sixel_decoder_unref(decoder);
3,178✔
1724
    if (logger_prepared) {
3,178✔
1725
        sixel_timeline_logger_unref(logger);
80✔
1726
    }
36✔
1727

1728
    return status;
4,356✔
1729
}
1,072✔
1730

1731

1732
/* Exercise legacy constructor and refcounting for the decoder. */
1733

1734
/* emacs Local Variables:      */
1735
/* emacs mode: c               */
1736
/* emacs tab-width: 4          */
1737
/* emacs indent-tabs-mode: nil */
1738
/* emacs c-basic-offset: 4     */
1739
/* emacs End:                  */
1740
/* vim: set expandtab ts=4 sts=4 sw=4 : */
1741
/* EOF */
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc