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

saitoha / libsixel / 22281294763

22 Feb 2026 04:53PM UTC coverage: 81.971% (-1.7%) from 83.691%
22281294763

push

github

saitoha
tests: enforce SIXEL_TEST_PYTHON contract for python tap runs

27660 of 54661 branches covered (50.6%)

45511 of 55521 relevant lines covered (81.97%)

2582260.61 hits per line

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

73.46
/src/loader-libwebp.c
1
/*
2
 * SPDX-License-Identifier: MIT
3
 *
4
 * Copyright (c) 2025 libsixel developers. See `AUTHORS`.
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to
8
 * deal in the Software without restriction, including without limitation the
9
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
 * sell copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all 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,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22
 * DEALINGS IN THE SOFTWARE.
23
 *
24
 * libwebp-backed loader helpers extracted from loader.c to keep WebP decoding
25
 * isolated from unrelated translation units.
26
 */
27

28
#if defined(HAVE_CONFIG_H)
29
#include "config.h"
30
#endif
31

32
#if HAVE_WEBP
33

34
#include <stdio.h>
35
#if HAVE_STRING_H
36
# include <string.h>
37
#endif
38
#if HAVE_ERRNO_H
39
# include <errno.h>
40
#endif
41
#if HAVE_LIMITS_H
42
# include <limits.h>
43
#endif
44
#if HAVE_STDLIB_H
45
# include <stdlib.h>
46
#endif
47
#include <webp/decode.h>
48
#include <webp/demux.h>
49

50
#include <sixel.h>
51

52
#include "allocator.h"
53
#include "chunk.h"
54
#include "frame.h"
55
#include "loader-common.h"
56
#include "loader-libwebp.h"
57
#include "logger.h"
58
#include "compat_stub.h"
59

60
/*
61
 * Decode a WebP buffer into an RGB(A) pixel buffer managed by libsixel.
62
 *
63
 * The steps are:
64
 *   1) Probe the WebP bitstream for dimensions and alpha flags.
65
 *   2) Allocate the output buffer from the sixel allocator.
66
 *   3) Decode into RGB or RGBA depending on the alpha information.
67
 */
68
static SIXELSTATUS
69
load_webp(unsigned char **result,
4✔
70
          unsigned char *data,
71
          size_t datasize,
72
          int *pwidth,
73
          int *pheight,
74
          int *ppixelformat,
75
          sixel_allocator_t *allocator)
76
{
77
    SIXELSTATUS status;
4✔
78
    WebPBitstreamFeatures features;
4✔
79
    int bytes_per_pixel;
4✔
80
    size_t stride;
4✔
81
    size_t size;
4✔
82

83
    status = SIXEL_BAD_INPUT;
4✔
84

85
    if (WebPGetFeatures(data, datasize, &features) != VP8_STATUS_OK) {
4!
86
        sixel_helper_set_additional_message(
×
87
            "load_webp: WebPGetFeatures failed.");
88
        return status;
×
89
    }
90

91
    if (features.width <= 0 || features.height <= 0) {
4!
92
        sixel_helper_set_additional_message(
×
93
            "load_webp: invalid image dimensions.");
94
        return status;
×
95
    }
96

97
    if (features.width > INT_MAX || features.height > INT_MAX) {
4!
98
        return SIXEL_BAD_INTEGER_OVERFLOW;
×
99
    }
100

101
    *pwidth = features.width;
4✔
102
    *pheight = features.height;
4✔
103

104
    bytes_per_pixel = features.has_alpha ? 4 : 3;
4✔
105
    *ppixelformat = features.has_alpha ?
4✔
106
        SIXEL_PIXELFORMAT_RGBA8888 : SIXEL_PIXELFORMAT_RGB888;
107

108
    if ((size_t)*pwidth > SIZE_MAX / (size_t)bytes_per_pixel) {
4!
109
        return SIXEL_BAD_INTEGER_OVERFLOW;
×
110
    }
111

112
    stride = (size_t)*pwidth * (size_t)bytes_per_pixel;
4✔
113
    if ((size_t)*pheight > 0 && stride > SIZE_MAX / (size_t)*pheight) {
4!
114
        return SIXEL_BAD_INTEGER_OVERFLOW;
×
115
    }
116

117
    size = stride * (size_t)*pheight;
4✔
118
    *result = (unsigned char *)sixel_allocator_malloc(allocator, size);
4✔
119
    if (*result == NULL) {
4!
120
        sixel_helper_set_additional_message(
×
121
            "load_webp: sixel_allocator_malloc() failed.");
122
        return SIXEL_BAD_ALLOCATION;
×
123
    }
124

125
    if (features.has_alpha) {
4!
126
        if (WebPDecodeRGBAInto(data, datasize, *result, size,
×
127
                               (int)stride) == NULL) {
×
128
            sixel_helper_set_additional_message(
×
129
                "load_webp: WebPDecodeRGBAInto failed.");
130
            return SIXEL_BAD_INPUT;
×
131
        }
132
    } else {
×
133
        if (WebPDecodeRGBInto(data, datasize, *result, size,
12!
134
                              (int)stride) == NULL) {
8✔
135
            sixel_helper_set_additional_message(
×
136
                "load_webp: WebPDecodeRGBInto failed.");
137
            return SIXEL_BAD_INPUT;
×
138
        }
139
    }
140

141
    status = SIXEL_OK;
4✔
142

143
    return status;
4✔
144
}
4✔
145

146

147
/*
148
 * Parse a VP8L payload header and detect whether the color indexing
149
 * transform is present.
150
 */
151
static int
152
vp8l_payload_uses_color_indexing(unsigned char const *data, size_t size)
60✔
153
{
154
    unsigned int bitbuf;
60✔
155
    int bits;
60✔
156
    size_t pos;
60✔
157
    unsigned int value;
60✔
158
    int transform_type;
60✔
159

160
    bitbuf = 0U;
60✔
161
    bits = 0;
60✔
162
    pos = 0U;
60✔
163
    value = 0U;
60✔
164
    transform_type = 0;
60✔
165

166
    if (data == NULL || size < 5U) {
60!
167
        return 0;
×
168
    }
169

170
    if (size >= 13U &&
120!
171
        data[0] == 'V' &&
60!
172
        data[1] == 'P' &&
60!
173
        data[2] == '8' &&
60!
174
        data[3] == 'L') {
60✔
175
        data += 8;
60✔
176
        size -= 8U;
60✔
177
    }
60✔
178

179
    if (data[0] != 0x2fU) {
60!
180
        return 0;
×
181
    }
182

183
    pos = 5U;
60✔
184

185
    for (;;) {
60✔
186
        while (bits < 1) {
120✔
187
            if (pos >= size) {
60!
188
                return 0;
×
189
            }
190
            bitbuf |= (unsigned int)data[pos] << bits;
60✔
191
            bits += 8;
60✔
192
            pos++;
60✔
193
        }
194
        value = bitbuf & 0x1U;
60✔
195
        bitbuf >>= 1;
60✔
196
        bits -= 1;
60✔
197
        if (value == 0U) {
60!
198
            break;
×
199
        }
200

201
        while (bits < 2) {
60!
202
            if (pos >= size) {
×
203
                return 0;
×
204
            }
205
            bitbuf |= (unsigned int)data[pos] << bits;
×
206
            bits += 8;
×
207
            pos++;
×
208
        }
209
        transform_type = (int)(bitbuf & 0x3U);
60✔
210
        bitbuf >>= 2;
60✔
211
        bits -= 2;
60✔
212

213
        if (transform_type == 3) {
60!
214
            return 1;
60✔
215
        }
216

217
        if (transform_type == 0 || transform_type == 1) {
×
218
            while (bits < 3) {
×
219
                if (pos >= size) {
×
220
                    return 0;
×
221
                }
222
                bitbuf |= (unsigned int)data[pos] << bits;
×
223
                bits += 8;
×
224
                pos++;
×
225
            }
226
            bitbuf >>= 3;
×
227
            bits -= 3;
×
228
        } else if (transform_type == 3) {
×
229
            while (bits < 8) {
×
230
                if (pos >= size) {
×
231
                    return 0;
×
232
                }
233
                bitbuf |= (unsigned int)data[pos] << bits;
×
234
                bits += 8;
×
235
                pos++;
×
236
            }
237
            bitbuf >>= 8;
×
238
            bits -= 8;
×
239
        }
×
240
    }
241

242
    return 0;
×
243
}
60✔
244

245

246
/*
247
 * Return 1 when every frame in the WebP stream uses VP8L color indexing.
248
 *
249
 * Palette promotion must only run for bitstreams that are explicitly indexed
250
 * in the source format. RGB/RGBA sources must keep their original semantics.
251
 */
252
static int
253
webp_stream_may_contain_vp8l(sixel_chunk_t const *pchunk)
38✔
254
{
255
    unsigned char const *bytes;
38✔
256
    size_t i;
38✔
257

258
    bytes = NULL;
38✔
259
    i = 0U;
38✔
260

261
    if (pchunk == NULL || pchunk->buffer == NULL || pchunk->size < 4U) {
38!
262
        return 0;
×
263
    }
264

265
    bytes = pchunk->buffer;
38✔
266
    for (i = 0U; i + 3U < pchunk->size; ++i) {
5,282✔
267
        if (bytes[i + 0U] == 'V' &&
5,354✔
268
            bytes[i + 1U] == 'P' &&
86✔
269
            bytes[i + 2U] == '8' &&
80!
270
            bytes[i + 3U] == 'L') {
80✔
271
            return 1;
30✔
272
        }
273
    }
5,244✔
274

275
    return 0;
8✔
276
}
38✔
277

278

279
static int
280
webp_input_is_indexed(sixel_chunk_t const *pchunk)
38✔
281
{
282
    WebPData data;
38✔
283
    WebPDemuxer *demux;
38✔
284
    WebPIterator iter;
38✔
285
    int frame_count;
38✔
286
    int frame_index;
38✔
287
    int indexed;
38✔
288

289
    data = (WebPData){ 0 };
38✔
290
    demux = NULL;
38✔
291
    iter = (WebPIterator){ 0 };
38✔
292
    frame_count = 0;
38✔
293
    frame_index = 0;
38✔
294
    indexed = 0;
38✔
295

296
    if (pchunk == NULL || pchunk->buffer == NULL || pchunk->size == 0U) {
38!
297
        return 0;
×
298
    }
299
    if (!webp_stream_may_contain_vp8l(pchunk)) {
38✔
300
        return 0;
8✔
301
    }
302

303
    data.bytes = pchunk->buffer;
30✔
304
    data.size = pchunk->size;
30✔
305

306
    demux = WebPDemux(&data);
30✔
307
    if (demux == NULL) {
30!
308
        return 0;
×
309
    }
310

311
    frame_count = (int)WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT);
30✔
312
    if (frame_count <= 0) {
30!
313
        WebPDemuxDelete(demux);
×
314
        return 0;
×
315
    }
316

317
    indexed = 1;
30✔
318
    for (frame_index = 1; frame_index <= frame_count; ++frame_index) {
90✔
319
        if (!WebPDemuxGetFrame(demux, frame_index, &iter)) {
60!
320
            indexed = 0;
×
321
            break;
×
322
        }
323

324
        if (!vp8l_payload_uses_color_indexing(iter.fragment.bytes,
120!
325
                                              iter.fragment.size)) {
60✔
326
            indexed = 0;
×
327
            WebPDemuxReleaseIterator(&iter);
×
328
            break;
×
329
        }
330

331
        WebPDemuxReleaseIterator(&iter);
60✔
332
    }
60✔
333

334
    WebPDemuxDelete(demux);
30✔
335

336
    return indexed;
30✔
337
}
38✔
338

339

340
/*
341
 * Try to convert an RGB frame into PAL8 when palette mode is requested.
342
 *
343
 * This helper performs an exact-color scan only. It does not approximate
344
 * colors. If the frame contains more unique colors than the requested budget,
345
 * the original RGB pixels are preserved.
346
 */
347
static SIXELSTATUS
348
webp_parse_animation_start_frame_no(int *start_frame_no)
36✔
349
{
350
    SIXELSTATUS status;
36✔
351
    char const *env_value;
36✔
352
    char *endptr;
36✔
353
    long parsed;
36✔
354

355
    status = SIXEL_OK;
36✔
356
    env_value = NULL;
36✔
357
    endptr = NULL;
36✔
358
    parsed = 0;
36✔
359

360
    *start_frame_no = INT_MIN;
36✔
361
    env_value = sixel_compat_getenv("SIXEL_LOADER_ANIMATION_START_FRAME_NO");
36✔
362
    if (env_value == NULL || env_value[0] == '\0') {
36!
363
        goto end;
36✔
364
    }
365

366
    parsed = strtol(env_value, &endptr, 10);
×
367
    if (endptr == env_value || *endptr != '\0') {
×
368
        sixel_helper_set_additional_message(
×
369
            "SIXEL_LOADER_ANIMATION_START_FRAME_NO must be an integer.");
370
        status = SIXEL_BAD_INPUT;
×
371
        goto end;
×
372
    }
373
    if (parsed < (long)INT_MIN || parsed > (long)INT_MAX) {
×
374
        sixel_helper_set_additional_message(
×
375
            "SIXEL_LOADER_ANIMATION_START_FRAME_NO is out of range.");
376
        status = SIXEL_BAD_INPUT;
×
377
        goto end;
×
378
    }
379

380
    *start_frame_no = (int)parsed;
×
381

382
end:
383
    return status;
72✔
384
}
36✔
385

386
static SIXELSTATUS
387
webp_resolve_animation_start_frame_no(int start_frame_no,
8✔
388
                                      int frame_count,
389
                                      int *resolved)
390
{
391
    SIXELSTATUS status;
8✔
392
    int index;
8✔
393

394
    status = SIXEL_OK;
8✔
395
    index = 0;
8✔
396

397
    if (frame_count <= 0) {
8!
398
        sixel_helper_set_additional_message(
×
399
            "Animation frame count must be positive.");
400
        status = SIXEL_BAD_INPUT;
×
401
        goto end;
×
402
    }
403

404
    if (start_frame_no >= 0) {
8✔
405
        index = start_frame_no;
4✔
406
    } else {
4✔
407
        index = frame_count + start_frame_no;
4✔
408
    }
409

410
    if (index < 0 || index >= frame_count) {
8✔
411
        sixel_helper_set_additional_message(
4✔
412
            "SIXEL_LOADER_ANIMATION_START_FRAME_NO is outside"
413
            " the animation frame range.");
414
        status = SIXEL_BAD_INPUT;
4✔
415
        goto end;
4✔
416
    }
417

418
    *resolved = index;
4✔
419

420
end:
421
    return status;
16✔
422
}
8✔
423

424

425
static SIXELSTATUS
426
loader_try_promote_pal8(
48✔
427
    sixel_frame_t  /* in/out */ *frame,
428
    int            /* in */     reqcolors,
429
    sixel_allocator_t /* in */  *allocator)
430
{
431
    SIXELSTATUS status;
48✔
432
    unsigned char *src;
48✔
433
    unsigned char *indices;
48✔
434
    unsigned char *palette;
48✔
435
    int pixel_total;
48✔
436
    int maxcolors;
48✔
437
    int i;
48✔
438
    int j;
48✔
439
    int index;
48✔
440
    int ncolors;
48✔
441
    int offset;
48✔
442
    int found;
48✔
443
    unsigned char r;
48✔
444
    unsigned char g;
48✔
445
    unsigned char b;
48✔
446
    unsigned int key;
48✔
447
    unsigned int mixed;
48✔
448
    unsigned int step;
48✔
449
    unsigned int slot;
48✔
450
    unsigned int probe;
48✔
451
    unsigned int table_size;
48✔
452
    unsigned int table_mask;
48✔
453
    unsigned int *keys;
48✔
454
    unsigned char *values;
48✔
455
    int lookup_index;
48✔
456

457
#define PAL8_HASH_EMPTY_KEY 0xffffffffU
458

459
    status = SIXEL_OK;
48✔
460
    src = NULL;
48✔
461
    indices = NULL;
48✔
462
    palette = NULL;
48✔
463
    pixel_total = 0;
48✔
464
    maxcolors = 0;
48✔
465
    i = 0;
48✔
466
    j = 0;
48✔
467
    index = 0;
48✔
468
    ncolors = 0;
48✔
469
    offset = 0;
48✔
470
    found = 0;
48✔
471
    r = 0;
48✔
472
    g = 0;
48✔
473
    b = 0;
48✔
474
    key = 0U;
48✔
475
    mixed = 0U;
48✔
476
    step = 0U;
48✔
477
    slot = 0U;
48✔
478
    probe = 0U;
48✔
479
    table_size = 0U;
48✔
480
    table_mask = 0U;
48✔
481
    keys = NULL;
48✔
482
    values = NULL;
48✔
483
    lookup_index = 0;
48✔
484

485
    if (frame == NULL || allocator == NULL) {
48!
486
        return SIXEL_BAD_ARGUMENT;
×
487
    }
488
    if (frame->pixelformat != SIXEL_PIXELFORMAT_RGB888) {
48!
489
        return SIXEL_OK;
×
490
    }
491
    if (frame->width <= 0 || frame->height <= 0) {
48!
492
        return SIXEL_BAD_INPUT;
×
493
    }
494

495
    pixel_total = frame->width * frame->height;
48✔
496
    if (pixel_total / frame->width != frame->height) {
48!
497
        return SIXEL_BAD_INTEGER_OVERFLOW;
×
498
    }
499

500
    maxcolors = reqcolors;
48✔
501
    if (maxcolors <= 0 || maxcolors > SIXEL_PALETTE_MAX) {
48!
502
        maxcolors = SIXEL_PALETTE_MAX;
×
503
    }
×
504

505
    src = frame->pixels.u8ptr;
48✔
506
    if (src == NULL) {
48!
507
        return SIXEL_BAD_INPUT;
×
508
    }
509

510
    indices = (unsigned char *)sixel_allocator_malloc(allocator,
96✔
511
                                                      (size_t)pixel_total);
48✔
512
    if (indices == NULL) {
48!
513
        return SIXEL_BAD_ALLOCATION;
×
514
    }
515
    palette = (unsigned char *)sixel_allocator_malloc(allocator,
96✔
516
                                                      (size_t)maxcolors * 3U);
48✔
517
    if (palette == NULL) {
48!
518
        sixel_allocator_free(allocator, indices);
×
519
        return SIXEL_BAD_ALLOCATION;
×
520
    }
521

522
    table_size = 2048U;
48✔
523
    while (table_size < (unsigned int)maxcolors * 4U) {
48!
524
        table_size <<= 1U;
×
525
    }
526
    table_mask = table_size - 1U;
48✔
527

528
    keys = (unsigned int *)sixel_allocator_malloc(allocator,
96✔
529
                                                  sizeof(unsigned int) *
48✔
530
                                                  table_size);
48✔
531
    if (keys == NULL) {
48!
532
        sixel_allocator_free(allocator, palette);
×
533
        sixel_allocator_free(allocator, indices);
×
534
        return SIXEL_BAD_ALLOCATION;
×
535
    }
536
    values = (unsigned char *)sixel_allocator_malloc(allocator,
96✔
537
                                                     table_size);
48✔
538
    if (values == NULL) {
48!
539
        sixel_allocator_free(allocator, keys);
×
540
        sixel_allocator_free(allocator, palette);
×
541
        sixel_allocator_free(allocator, indices);
×
542
        return SIXEL_BAD_ALLOCATION;
×
543
    }
544

545
    for (i = 0; i < (int)table_size; ++i) {
98,352✔
546
        keys[i] = PAL8_HASH_EMPTY_KEY;
98,304✔
547
        values[i] = 0U;
98,304✔
548
    }
98,304✔
549

550
    for (i = 0; i < pixel_total; ++i) {
3,120✔
551
        offset = i * 3;
3,072✔
552
        r = src[offset + 0];
3,072✔
553
        g = src[offset + 1];
3,072✔
554
        b = src[offset + 2];
3,072✔
555
        key = ((unsigned int)r << 16)
6,144✔
556
            | ((unsigned int)g << 8)
3,072✔
557
            | (unsigned int)b;
3,072✔
558
        mixed = key;
3,072✔
559
        mixed ^= mixed >> 13;
3,072✔
560
        mixed *= 0x9e3779b1U;
3,072✔
561
        mixed ^= mixed >> 16;
3,072✔
562
        step = 0U;
3,072✔
563
        found = 0;
3,072✔
564
        index = 0;
3,072✔
565

566
        for (;;) {
3,072✔
567
            slot = (mixed + step) & table_mask;
3,072✔
568
            probe = keys[slot];
3,072✔
569
            if (probe == PAL8_HASH_EMPTY_KEY) {
3,072✔
570
                break;
48✔
571
            }
572
            if (probe == key) {
3,024!
573
                lookup_index = (int)values[slot];
3,024✔
574
                if (lookup_index < ncolors &&
6,048!
575
                    palette[lookup_index * 3 + 0] == r &&
3,024!
576
                    palette[lookup_index * 3 + 1] == g &&
3,024!
577
                    palette[lookup_index * 3 + 2] == b) {
3,024✔
578
                    found = 1;
3,024✔
579
                    index = lookup_index;
3,024✔
580
                    break;
3,024✔
581
                }
582
            }
×
583
            step++;
×
584
            if (step > table_mask) {
×
585
                break;
×
586
            }
587
        }
588

589
        if (!found && step > table_mask) {
3,072!
590
            for (j = 0; j < ncolors; ++j) {
×
591
                if (palette[j * 3 + 0] == r &&
×
592
                    palette[j * 3 + 1] == g &&
×
593
                    palette[j * 3 + 2] == b) {
×
594
                    found = 1;
×
595
                    index = j;
×
596
                    break;
×
597
                }
598
            }
×
599
        }
×
600

601
        if (!found) {
3,072✔
602
            if (ncolors >= maxcolors) {
48!
603
                sixel_allocator_free(allocator, values);
×
604
                sixel_allocator_free(allocator, keys);
×
605
                sixel_allocator_free(allocator, palette);
×
606
                sixel_allocator_free(allocator, indices);
×
607
                return SIXEL_OK;
×
608
            }
609
            index = ncolors;
48✔
610
            palette[index * 3 + 0] = r;
48✔
611
            palette[index * 3 + 1] = g;
48✔
612
            palette[index * 3 + 2] = b;
48✔
613
            ncolors++;
48✔
614

615
            if (step <= table_mask) {
48!
616
                keys[slot] = key;
48✔
617
                values[slot] = (unsigned char)index;
48✔
618
            }
48✔
619
        }
48✔
620

621
        indices[i] = (unsigned char)index;
3,072✔
622
    }
3,072✔
623

624
    sixel_allocator_free(allocator, values);
48✔
625
    sixel_allocator_free(allocator, keys);
48✔
626

627
    sixel_allocator_free(allocator, frame->pixels.u8ptr);
48✔
628
    frame->pixels.u8ptr = NULL;
48✔
629

630
    sixel_frame_set_pixels(frame, indices);
48✔
631
    sixel_frame_set_palette(frame, palette);
48✔
632
    frame->ncolors = ncolors;
48✔
633
    frame->pixelformat = SIXEL_PIXELFORMAT_PAL8;
48✔
634
    frame->colorspace = SIXEL_COLORSPACE_GAMMA;
48✔
635

636
    return status;
48✔
637

638
#undef PAL8_HASH_EMPTY_KEY
639
}
48✔
640

641
/*
642
 * Dedicated libwebp loader wiring minimal pipeline.
643
 *
644
 *    +------------+     +-------------------+     +--------------------+
645
 *    | WebP chunk | --> | libwebp decode    | --> | sixel frame emit   |
646
 *    +------------+     +-------------------+     +--------------------+
647
 */
648
SIXELSTATUS
649
load_with_libwebp(
44✔
650
    sixel_chunk_t const       /* in */     *pchunk,
651
    int                       /* in */     fstatic,
652
    int                       /* in */     fuse_palette,
653
    int                       /* in */     reqcolors,
654
    unsigned char             /* in */     *bgcolor,
655
    int                       /* in */     loop_control,
656
    int                       /* in */     start_frame_no_set,
657
    int                       /* in */     start_frame_no_override,
658
    sixel_load_image_function /* in */     fn_load,
659
    void                      /* in/out */ *context)
660
{
661
    SIXELSTATUS status;
44✔
662
    sixel_frame_t *frame;
44✔
663
    unsigned char *pixels;
44✔
664
    WebPData webp_data;
44✔
665
    WebPAnimDecoderOptions decoder_options;
44✔
666
    WebPAnimDecoder *decoder;
44✔
667
    WebPAnimInfo anim_info;
44✔
668
    uint8_t *decoded_frame;
44✔
669
    int timestamp;
44✔
670
    int previous_timestamp;
44✔
671
    size_t frame_bytes;
44✔
672
    int next_delay;
44✔
673
    int frame_no;
44✔
674
    int loop_count;
44✔
675
    int allow_palette_promotion;
44✔
676
    int start_frame_no;
44✔
677
    int resolved_start_frame_no;
44✔
678
    int decode_start_frame_no;
44✔
679
    int emitted_frame_no;
44✔
680

681
    status = SIXEL_FALSE;
44✔
682
    frame = NULL;
44✔
683
    pixels = NULL;
44✔
684
    webp_data = (WebPData){ 0 };
44✔
685
    decoder = NULL;
44✔
686
    anim_info = (WebPAnimInfo){ 0 };
44✔
687
    decoded_frame = NULL;
44✔
688
    timestamp = 0;
44✔
689
    previous_timestamp = 0;
44✔
690
    frame_bytes = 0;
44✔
691
    next_delay = 0;
44✔
692
    frame_no = 0;
44✔
693
    loop_count = 0;
44✔
694
    allow_palette_promotion = 0;
44✔
695
    start_frame_no = INT_MIN;
44✔
696
    resolved_start_frame_no = 0;
44✔
697
    decode_start_frame_no = 0;
44✔
698
    emitted_frame_no = 0;
44✔
699

700
    if (start_frame_no_set) {
44✔
701
        start_frame_no = start_frame_no_override;
8✔
702
    } else {
8✔
703
        status = webp_parse_animation_start_frame_no(&start_frame_no);
36✔
704
        if (SIXEL_FAILED(status)) {
36!
705
            goto end;
×
706
        }
707
    }
708

709
    if (fuse_palette) {
44✔
710
        allow_palette_promotion = webp_input_is_indexed(pchunk);
38✔
711
    }
38✔
712

713
    webp_data.bytes = pchunk->buffer;
44✔
714
    webp_data.size = pchunk->size;
44✔
715

716
    if (!WebPAnimDecoderOptionsInit(&decoder_options)) {
44!
717
        sixel_helper_set_additional_message(
×
718
            "load_with_libwebp: WebPAnimDecoderOptionsInit failed.");
719
        status = SIXEL_WEBP_ERROR;
×
720
        goto end;
×
721
    }
722
    decoder_options.color_mode = MODE_RGBA;
44✔
723
    decoder = WebPAnimDecoderNew(&webp_data, &decoder_options);
44✔
724
    if (decoder == NULL) {
44!
725
        sixel_helper_set_additional_message(
×
726
            "load_with_libwebp: WebPAnimDecoderNew failed.");
727
        status = SIXEL_WEBP_ERROR;
×
728
        goto end;
×
729
    }
730

731
    if (!WebPAnimDecoderGetInfo(decoder, &anim_info)) {
44!
732
        sixel_helper_set_additional_message(
×
733
            "load_with_libwebp: WebPAnimDecoderGetInfo failed.");
734
        status = SIXEL_WEBP_ERROR;
×
735
        goto end;
×
736
    }
737

738
    if (anim_info.frame_count <= 1) {
44✔
739
        if (start_frame_no != INT_MIN) {
4!
740
            status = webp_resolve_animation_start_frame_no(start_frame_no,
×
741
                            anim_info.frame_count,
×
742
                            &resolved_start_frame_no);
743
            if (SIXEL_FAILED(status)) {
×
744
                goto end;
×
745
            }
746
        }
×
747
        WebPAnimDecoderDelete(decoder);
4✔
748
        decoder = NULL;
4✔
749

750
        status = sixel_frame_new(&frame, pchunk->allocator);
4✔
751
        if (SIXEL_FAILED(status)) {
4!
752
            goto end;
×
753
        }
754

755
        status = load_webp(&pixels,
4✔
756
                           pchunk->buffer,
4✔
757
                           pchunk->size,
4✔
758
                           &frame->width,
4✔
759
                           &frame->height,
4✔
760
                           &frame->pixelformat,
4✔
761
                           pchunk->allocator);
4✔
762
        if (SIXEL_FAILED(status)) {
4!
763
            goto end;
×
764
        }
765

766
        sixel_frame_set_pixels(frame, pixels);
4✔
767

768
        status = sixel_frame_strip_alpha(frame, bgcolor);
4✔
769
        if (SIXEL_FAILED(status)) {
4!
770
            goto end;
×
771
        }
772

773
        if (allow_palette_promotion) {
4!
774
            status = loader_try_promote_pal8(frame,
×
775
                                             reqcolors,
×
776
                                             pchunk->allocator);
×
777
            if (SIXEL_FAILED(status)) {
×
778
                goto end;
×
779
            }
780
        }
×
781

782
        status = fn_load(frame, context);
4✔
783
        if (SIXEL_FAILED(status)) {
4!
784
            goto end;
×
785
        }
786

787
        status = SIXEL_OK;
4✔
788
        goto end;
4✔
789
    }
790

791
    if (fstatic) {
40✔
792
        if (start_frame_no != INT_MIN) {
6!
793
            status = webp_resolve_animation_start_frame_no(start_frame_no,
×
794
                            anim_info.frame_count,
×
795
                            &resolved_start_frame_no);
796
            if (SIXEL_FAILED(status)) {
×
797
                goto end;
×
798
            }
799
        }
×
800

801
        status = sixel_frame_new(&frame, pchunk->allocator);
6✔
802
        if (SIXEL_FAILED(status)) {
6!
803
            goto end;
×
804
        }
805

806
        for (frame_no = 0; frame_no <= resolved_start_frame_no; frame_no++) {
12✔
807
            if (!WebPAnimDecoderHasMoreFrames(decoder)) {
6!
808
                sixel_helper_set_additional_message(
×
809
                    "load_with_libwebp: no frames in animated WebP stream.");
810
                status = SIXEL_BAD_INPUT;
×
811
                goto end;
×
812
            }
813
            if (!WebPAnimDecoderGetNext(decoder, &decoded_frame, &timestamp)) {
6!
814
                sixel_helper_set_additional_message(
×
815
                    "load_with_libwebp: WebPAnimDecoderGetNext failed.");
816
                status = SIXEL_WEBP_ERROR;
×
817
                goto end;
×
818
            }
819
        }
6✔
820

821
        frame->width = (int)anim_info.canvas_width;
6✔
822
        frame->height = (int)anim_info.canvas_height;
6✔
823
        frame->pixelformat = SIXEL_PIXELFORMAT_RGBA8888;
6✔
824
        frame->colorspace = SIXEL_COLORSPACE_GAMMA;
6✔
825
        frame->multiframe = 0;
6✔
826
        frame->loop_count = 0;
6✔
827
        frame->frame_no = resolved_start_frame_no;
6✔
828
        frame->delay = timestamp / 10;
6✔
829

830
        if (frame->width <= 0 || frame->height <= 0) {
6!
831
            sixel_helper_set_additional_message(
×
832
                "load_with_libwebp: invalid canvas dimensions.");
833
            status = SIXEL_BAD_INPUT;
×
834
            goto end;
×
835
        }
836
        if ((size_t)frame->width > SIZE_MAX / 4 ||
6!
837
            (size_t)frame->height > SIZE_MAX / ((size_t)frame->width * 4)) {
6✔
838
            status = SIXEL_BAD_INTEGER_OVERFLOW;
×
839
            goto end;
×
840
        }
841
        frame_bytes = (size_t)frame->width * (size_t)frame->height * 4;
6✔
842

843
        pixels = (unsigned char *)sixel_allocator_malloc(pchunk->allocator,
12✔
844
                                                         frame_bytes);
6✔
845
        if (pixels == NULL) {
6!
846
            sixel_helper_set_additional_message(
×
847
                "load_with_libwebp: sixel_allocator_malloc() failed.");
848
            status = SIXEL_BAD_ALLOCATION;
×
849
            goto end;
×
850
        }
851
        memcpy(pixels, decoded_frame, frame_bytes);
6✔
852
        sixel_frame_set_pixels(frame, pixels);
6✔
853

854
        status = sixel_frame_strip_alpha(frame, bgcolor);
6✔
855
        if (SIXEL_FAILED(status)) {
6!
856
            goto end;
×
857
        }
858

859
        if (allow_palette_promotion) {
6✔
860
            status = loader_try_promote_pal8(frame,
8✔
861
                                             reqcolors,
4✔
862
                                             pchunk->allocator);
4✔
863
            if (SIXEL_FAILED(status)) {
4!
864
                goto end;
×
865
            }
866
        }
4✔
867

868
        status = fn_load(frame, context);
6✔
869
        if (SIXEL_FAILED(status)) {
6!
870
            goto end;
×
871
        }
872

873
        status = SIXEL_OK;
6✔
874
        goto end;
6✔
875
    }
876

877
    /*
878
     * Decode WebP animation as fully composited RGBA canvases.
879
     *
880
     *   outer loop : logical animation loop
881
     *   inner loop : frame traversal inside a single loop
882
     *
883
     * Create a fresh sixel_frame_t for each callback invocation. This keeps
884
     * frame state isolated and avoids leaking in-place updates from one frame
885
     * into the next frame.
886
     */
887
    if ((int)anim_info.canvas_width <= 0 ||
34!
888
        (int)anim_info.canvas_height <= 0) {
34✔
889
        sixel_helper_set_additional_message(
×
890
            "load_with_libwebp: invalid canvas dimensions.");
891
        status = SIXEL_BAD_INPUT;
×
892
        goto end;
×
893
    }
894
    frame_bytes = (size_t)anim_info.canvas_width *
68✔
895
                  (size_t)anim_info.canvas_height;
34✔
896
    if ((size_t)anim_info.canvas_width != 0 &&
34!
897
        frame_bytes / (size_t)anim_info.canvas_width !=
68✔
898
        (size_t)anim_info.canvas_height) {
34✔
899
        status = SIXEL_BAD_INTEGER_OVERFLOW;
×
900
        goto end;
×
901
    }
902
    if (frame_bytes > SIZE_MAX / 4) {
34!
903
        status = SIXEL_BAD_INTEGER_OVERFLOW;
×
904
        goto end;
×
905
    }
906
    frame_bytes *= 4;
34✔
907

908
    if (start_frame_no != INT_MIN) {
34✔
909
        status = webp_resolve_animation_start_frame_no(start_frame_no,
16✔
910
                        anim_info.frame_count,
8✔
911
                        &resolved_start_frame_no);
912
        if (SIXEL_FAILED(status)) {
8✔
913
            goto end;
4✔
914
        }
915
    }
4✔
916

917
    for (;;) {
32✔
918
        decode_start_frame_no = 0;
32✔
919
        if (loop_count == 0 && start_frame_no != INT_MIN) {
32✔
920
            decode_start_frame_no = resolved_start_frame_no;
4✔
921
        }
4✔
922

923
        frame_no = 0;
32✔
924
        emitted_frame_no = 0;
32✔
925
        previous_timestamp = 0;
32✔
926

927
        while (WebPAnimDecoderHasMoreFrames(decoder)) {
92✔
928
            if (!WebPAnimDecoderGetNext(decoder,
62!
929
                                        &decoded_frame,
930
                                        &timestamp)) {
931
                sixel_helper_set_additional_message(
×
932
                    "load_with_libwebp: WebPAnimDecoderGetNext failed.");
933
                status = SIXEL_WEBP_ERROR;
×
934
                goto end;
×
935
            }
936

937
            if (frame_no < decode_start_frame_no) {
62✔
938
                previous_timestamp = timestamp;
4✔
939
                frame_no++;
4✔
940
                continue;
4✔
941
            }
942

943
            status = sixel_frame_new(&frame, pchunk->allocator);
58✔
944
            if (SIXEL_FAILED(status)) {
58!
945
                goto end;
×
946
            }
947

948
            pixels = (unsigned char *)sixel_allocator_malloc(
58✔
949
                pchunk->allocator,
58✔
950
                frame_bytes);
58✔
951
            if (pixels == NULL) {
58!
952
                sixel_helper_set_additional_message(
×
953
                    "load_with_libwebp: sixel_allocator_malloc() failed.");
954
                status = SIXEL_BAD_ALLOCATION;
×
955
                goto end;
×
956
            }
957

958
            memcpy(pixels, decoded_frame, frame_bytes);
58✔
959
            sixel_frame_set_pixels(frame, pixels);
58✔
960
            pixels = NULL;
58✔
961

962
            frame->width = (int)anim_info.canvas_width;
58✔
963
            frame->height = (int)anim_info.canvas_height;
58✔
964
            frame->pixelformat = SIXEL_PIXELFORMAT_RGBA8888;
58✔
965
            frame->colorspace = SIXEL_COLORSPACE_GAMMA;
58✔
966
            frame->multiframe = 1;
58✔
967
            frame->loop_count = loop_count;
58✔
968
            frame->frame_no = emitted_frame_no;
58✔
969

970
            next_delay = timestamp - previous_timestamp;
58✔
971
            if (next_delay < 0) {
58!
972
                next_delay = 0;
×
973
            }
×
974
            frame->delay = next_delay / 10;
58✔
975

976
            status = sixel_frame_strip_alpha(frame, bgcolor);
58✔
977
            if (SIXEL_FAILED(status)) {
58!
978
                goto end;
×
979
            }
980

981
            if (allow_palette_promotion) {
58✔
982
                status = loader_try_promote_pal8(frame,
88✔
983
                                                 reqcolors,
44✔
984
                                                 pchunk->allocator);
44✔
985
                if (SIXEL_FAILED(status)) {
44!
986
                    goto end;
×
987
                }
988
            }
44✔
989

990
            status = fn_load(frame, context);
58✔
991
            if (status == SIXEL_INTERRUPTED) {
58✔
992
                goto end;
2✔
993
            }
994
            if (SIXEL_FAILED(status)) {
56!
995
                goto end;
×
996
            }
997

998
            sixel_frame_unref(frame);
56✔
999
            frame = NULL;
56✔
1000

1001
            previous_timestamp = timestamp;
56✔
1002
            emitted_frame_no++;
56✔
1003
            frame_no++;
56✔
1004
        }
1005

1006
        loop_count++;
30✔
1007

1008
        if (loop_control == SIXEL_LOOP_DISABLE || emitted_frame_no == 1) {
30!
1009
            break;
26✔
1010
        }
1011
        if (loop_control == SIXEL_LOOP_AUTO &&
8!
1012
            anim_info.loop_count > 0 &&
4!
1013
            (unsigned int)loop_count >= anim_info.loop_count) {
4✔
1014
            break;
2✔
1015
        }
1016

1017
        WebPAnimDecoderReset(decoder);
2✔
1018
    }
1019

1020
    status = SIXEL_OK;
28✔
1021

1022
end:
1023
    if (decoder != NULL) {
44✔
1024
        WebPAnimDecoderDelete(decoder);
40✔
1025
    }
40✔
1026
    sixel_frame_unref(frame);
44✔
1027

1028
    return status;
88✔
1029
}
44✔
1030

1031
int
1032
loader_can_try_libwebp(sixel_chunk_t const *chunk)
1,137✔
1033
{
1034
    if (chunk == NULL) {
1,137!
1035
        return 0;
×
1036
    }
1037

1038
    return chunk_is_webp(chunk);
1,137✔
1039
}
1,137✔
1040

1041
#else  /* !HAVE_WEBP */
1042

1043
/*
1044
 * Keep a harmless placeholder around so pedantic builds skip the empty unit
1045
 * warning when libwebp is not part of the build.
1046
 */
1047
enum { sixel_loader_libwebp_placeholder = 0 };
1048

1049
#if defined(__GNUC__) || defined(__clang__)
1050
# define SIXEL_LIBWEBP_PLACEHOLDER_UNUSED __attribute__((unused))
1051
#else
1052
# define SIXEL_LIBWEBP_PLACEHOLDER_UNUSED
1053
#endif
1054

1055
static void
1056
sixel_loader_libwebp_placeholder_function(void)
1057
    SIXEL_LIBWEBP_PLACEHOLDER_UNUSED;
1058

1059
static void
1060
sixel_loader_libwebp_placeholder_function(void)
1061
{
1062
    /*
1063
     * The placeholder ties the enum to a symbol so MSVC does not warn about
1064
     * an empty translation unit when libwebp support is disabled.
1065
     */
1066
    (void)sixel_loader_libwebp_placeholder;
1067
}
1068

1069
#undef SIXEL_LIBWEBP_PLACEHOLDER_UNUSED
1070

1071
#endif  /* HAVE_WEBP */
1072

1073
/* emacs Local Variables:      */
1074
/* emacs mode: c               */
1075
/* emacs tab-width: 4          */
1076
/* emacs indent-tabs-mode: nil */
1077
/* emacs c-basic-offset: 4     */
1078
/* emacs End:                  */
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