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

saitoha / libsixel / 22256663556

21 Feb 2026 12:16PM UTC coverage: 82.856% (-0.02%) from 82.877%
22256663556

push

github

saitoha
fix wic loader signature and start-frame override args

25673 of 50202 branches covered (51.14%)

45851 of 55338 relevant lines covered (82.86%)

4080301.46 hits per line

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

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

30
#if defined(HAVE_CONFIG_H)
31
#include "config.h"
32
#endif
33

34
#if HAVE_COREGRAPHICS
35

36
#include <stdio.h>
37
#include <limits.h>
38
#include <stdlib.h>
39

40
#if HAVE_STRING_H
41
# include <string.h>
42
#endif
43

44
#include <ApplicationServices/ApplicationServices.h>
45
#include <ImageIO/ImageIO.h>
46

47
#include <sixel.h>
48

49
#include "chunk.h"
50
#include "frame.h"
51
#include "loader-coregraphics.h"
52
#include "loader.h"
53
#include "logger.h"
54
#include "compat_stub.h"
55

56
static SIXELSTATUS
57
coregraphics_parse_animation_start_frame_no(int *start_frame_no)
84✔
58
{
59
    SIXELSTATUS status;
48✔
60
    char const *env_value;
48✔
61
    char *endptr;
48✔
62
    long parsed;
48✔
63

64
    status = SIXEL_OK;
84✔
65
    env_value = NULL;
84✔
66
    endptr = NULL;
84✔
67
    parsed = 0;
84✔
68

69
    *start_frame_no = INT_MIN;
84✔
70
    env_value = sixel_compat_getenv("SIXEL_LOADER_ANIMATION_START_FRAME_NO");
84✔
71
    if (env_value == NULL || env_value[0] == '\0') {
84!
72
        goto end;
84✔
73
    }
74

75
    parsed = strtol(env_value, &endptr, 10);
×
76
    if (endptr == env_value || *endptr != '\0') {
×
77
        sixel_helper_set_additional_message(
×
78
            "SIXEL_LOADER_ANIMATION_START_FRAME_NO must be an integer.");
79
        status = SIXEL_BAD_INPUT;
×
80
        goto end;
×
81
    }
82
    if (parsed < (long)INT_MIN || parsed > (long)INT_MAX) {
×
83
        sixel_helper_set_additional_message(
×
84
            "SIXEL_LOADER_ANIMATION_START_FRAME_NO is out of range.");
85
        status = SIXEL_BAD_INPUT;
×
86
        goto end;
×
87
    }
88

89
    *start_frame_no = (int)parsed;
×
90

91
end:
92
    return status;
132✔
93
}
48✔
94

95
static SIXELSTATUS
96
coregraphics_resolve_animation_start_frame_no(int start_frame_no,
42✔
97
                                              int frame_count,
98
                                              int *resolved)
99
{
100
    SIXELSTATUS status;
24✔
101
    int index;
24✔
102

103
    status = SIXEL_OK;
42✔
104
    index = 0;
42✔
105

106
    if (frame_count <= 0) {
42!
107
        sixel_helper_set_additional_message(
×
108
            "Animation frame count must be positive.");
109
        status = SIXEL_BAD_INPUT;
×
110
        goto end;
×
111
    }
112

113
    if (start_frame_no >= 0) {
42✔
114
        index = start_frame_no;
28✔
115
    } else {
28✔
116
        index = frame_count + start_frame_no;
14✔
117
    }
118

119
    if (index < 0 || index >= frame_count) {
42✔
120
        sixel_helper_set_additional_message(
14✔
121
            "SIXEL_LOADER_ANIMATION_START_FRAME_NO is outside"
122
            " the animation frame range.");
123
        status = SIXEL_BAD_INPUT;
14✔
124
        goto end;
14✔
125
    }
126

127
    *resolved = index;
28✔
128

129
end:
130
    return status;
66✔
131
}
24✔
132

133
SIXELSTATUS
134
load_with_coregraphics(
126✔
135
    sixel_chunk_t const       /* in */     *pchunk,
136
    int                       /* in */     fstatic,
137
    int                       /* in */     fuse_palette,
138
    int                       /* in */     reqcolors,
139
    unsigned char             /* in */     *bgcolor,
140
    int                       /* in */     loop_control,
141
    int                       /* in */     start_frame_no_set,
142
    int                       /* in */     start_frame_no_override,
143
    sixel_load_image_function /* in */     fn_load,
144
    void                      /* in/out */ *context)
145
{
146
    SIXELSTATUS status = SIXEL_FALSE;
126✔
147
    sixel_frame_t *frame = NULL;
126✔
148
    CFDataRef data = NULL;
126✔
149
    CGImageSourceRef source = NULL;
126✔
150
    CGImageRef image = NULL;
126✔
151
    CGColorSpaceRef color_space = NULL;
126✔
152
    CGContextRef ctx = NULL;
126✔
153
    size_t stride;
72✔
154
    size_t frame_count;
72✔
155
    int anim_loop_count = (-1);
126✔
156
    CFDictionaryRef props = NULL;
126✔
157
    CFDictionaryRef anim_dict;
72✔
158
    CFNumberRef loop_num;
72✔
159
    CFDictionaryRef frame_props;
72✔
160
    CFDictionaryRef frame_anim_dict;
72✔
161
    CFNumberRef delay_num;
72✔
162
    double delay_sec;
72✔
163
    unsigned char *pixels;
72✔
164
    int start_frame_no;
72✔
165
    int resolved_start_frame_no;
72✔
166
    int total_frames;
72✔
167
    int frame_index;
72✔
168
    int frames_in_loop;
72✔
169
    int loop_no;
72✔
170
    int stop_loop;
72✔
171

172
    (void) fuse_palette;
126✔
173
    (void) reqcolors;
126✔
174
    (void) bgcolor;
126✔
175

176
    start_frame_no = INT_MIN;
126✔
177
    resolved_start_frame_no = INT_MIN;
126✔
178
    total_frames = 0;
126✔
179
    frame_index = 0;
126✔
180
    frames_in_loop = 0;
126✔
181
    loop_no = 0;
126✔
182
    stop_loop = 0;
126✔
183
    frame_props = NULL;
126✔
184

185
    if (start_frame_no_set) {
126✔
186
        start_frame_no = start_frame_no_override;
42✔
187
    } else {
42✔
188
        status = coregraphics_parse_animation_start_frame_no(&start_frame_no);
84✔
189
        if (SIXEL_FAILED(status)) {
84!
190
            goto end;
×
191
        }
192
    }
193

194
    status = sixel_frame_new(&frame, pchunk->allocator);
126✔
195
    if (SIXEL_FAILED(status)) {
126!
196
        goto end;
×
197
    }
198

199
    data = CFDataCreate(kCFAllocatorDefault,
252✔
200
                        pchunk->buffer,
126✔
201
                        (CFIndex)pchunk->size);
126✔
202
    if (! data) {
126✔
203
        status = SIXEL_FALSE;
×
204
        goto end;
×
205
    }
206

207
    source = CGImageSourceCreateWithData(data, NULL);
126✔
208
    if (! source) {
126✔
209
        status = SIXEL_FALSE;
×
210
        goto end;
×
211
    }
212

213
    frame_count = CGImageSourceGetCount(source);
126✔
214
    if (! frame_count) {
126✔
215
        status = SIXEL_FALSE;
63✔
216
        goto end;
63✔
217
    }
218

219
    total_frames = (int)frame_count;
63✔
220
    if (start_frame_no != INT_MIN) {
63✔
221
        status = coregraphics_resolve_animation_start_frame_no(
42✔
222
            start_frame_no,
42✔
223
            total_frames,
42✔
224
            &resolved_start_frame_no);
225
        if (SIXEL_FAILED(status)) {
42✔
226
            goto end;
14✔
227
        }
228
    }
28✔
229

230
    /*
231
     * Keep total_frames as the actual image frame count even in static mode.
232
     * In static mode we still need to seek to resolved_start_frame_no first,
233
     * then emit exactly one frame and return from inside the decode loop.
234
     */
235

236
    props = CGImageSourceCopyProperties(source, NULL);
49✔
237
    if (props) {
49!
238
        anim_dict = (CFDictionaryRef)CFDictionaryGetValue(
49✔
239
            props, kCGImagePropertyGIFDictionary);
49✔
240
        if (anim_dict) {
49✔
241
            loop_num = (CFNumberRef)CFDictionaryGetValue(
42✔
242
                anim_dict, kCGImagePropertyGIFLoopCount);
42✔
243
            if (loop_num) {
42!
244
                CFNumberGetValue(loop_num, kCFNumberIntType, &anim_loop_count);
42✔
245
            }
42✔
246
        }
42✔
247
    }
49✔
248

249
    frame->multiframe = (!fstatic && frame_count > 1);
52✔
250

251
    for (;;) {
49✔
252
        frame_index = 0;
49✔
253
        if (loop_no == 0 && resolved_start_frame_no != INT_MIN) {
49!
254
            /*
255
             * Apply start-frame override only on the first loop. Later loops
256
             * always restart from frame 0 to preserve normal replay behavior.
257
             */
258
            frame_index = resolved_start_frame_no;
28✔
259
        }
28✔
260
        frames_in_loop = 0;
49✔
261

262
        while (frame_index < total_frames) {
126✔
263
            frame->frame_no = frames_in_loop;
98✔
264
            frame->loop_count = loop_no;
98✔
265

266
            image = CGImageSourceCreateImageAtIndex(
98✔
267
                source, (unsigned int)frame_index, NULL);
98✔
268
            if (! image) {
98!
269
                status = SIXEL_FALSE;
×
270
                goto end;
×
271
            }
272

273
            frame_props = CGImageSourceCopyPropertiesAtIndex(
98✔
274
                source, (unsigned int)frame_index, NULL);
98✔
275
            if (frame_props) {
98!
276
                frame_anim_dict = (CFDictionaryRef)CFDictionaryGetValue(
98✔
277
                    frame_props, kCGImagePropertyGIFDictionary);
98✔
278
                if (frame_anim_dict) {
98✔
279
                    loop_num = (CFNumberRef)CFDictionaryGetValue(
91✔
280
                        frame_anim_dict, kCGImagePropertyGIFLoopCount);
91✔
281
                    if (loop_num) {
91!
282
                        CFNumberGetValue(loop_num,
×
283
                                         kCFNumberIntType,
284
                                         &anim_loop_count);
285
                    }
×
286
                    delay_num = (CFNumberRef)CFDictionaryGetValue(
78✔
287
                        frame_anim_dict,
78✔
288
                        kCGImagePropertyGIFUnclampedDelayTime);
78✔
289
                    if (! delay_num) {
78!
290
                        delay_num = (CFNumberRef)CFDictionaryGetValue(
×
291
                            frame_anim_dict, kCGImagePropertyGIFDelayTime);
×
292
                    }
×
293
                    if (delay_num) {
91!
294
                        CFNumberGetValue(
91✔
295
                            delay_num,
91✔
296
                            kCFNumberDoubleType,
297
                            &delay_sec);
298
                        if (delay_sec < 0) {
91!
299
                            delay_sec = 0.0;
×
300
                        }
×
301
                        frame->delay = (int)(delay_sec * 100);
91✔
302
                    }
91✔
303
                }
91✔
304
                CFRelease(frame_props);
98✔
305
                frame_props = NULL;
98✔
306
            }
98✔
307

308
            frame->width = (int)CGImageGetWidth(image);
98✔
309
            frame->height = (int)CGImageGetHeight(image);
98✔
310
            /*
311
             * CoreGraphics renders into a premultiplied RGBA surface. Report
312
             * the four-component layout so downstream planners know an alpha
313
             * channel is available.
314
             */
315
            frame->pixelformat = SIXEL_PIXELFORMAT_RGBA8888;
98✔
316
            frame->colorspace = SIXEL_COLORSPACE_GAMMA;
98✔
317

318
            if (frame->width > SIXEL_WIDTH_LIMIT) {
98!
319
                sixel_helper_set_additional_message(
×
320
                    "load_with_coregraphics: given width parameter is too"
321
                    " huge.");
322
                status = SIXEL_BAD_INPUT;
×
323
                goto end;
×
324
            }
325
            if (frame->height > SIXEL_HEIGHT_LIMIT) {
98!
326
                sixel_helper_set_additional_message(
×
327
                    "load_with_coregraphics: given height parameter is too"
328
                    " huge.");
329
                status = SIXEL_BAD_INPUT;
×
330
                goto end;
×
331
            }
332
            if (frame->width <= 0) {
98!
333
                sixel_helper_set_additional_message(
×
334
                    "load_with_coregraphics: an invalid width parameter"
335
                    " detected.");
336
                status = SIXEL_BAD_INPUT;
×
337
                goto end;
×
338
            }
339
            if (frame->height <= 0) {
98!
340
                sixel_helper_set_additional_message(
×
341
                    "load_with_coregraphics: an invalid width parameter"
342
                    " detected.");
343
                status = SIXEL_BAD_INPUT;
×
344
                goto end;
×
345
            }
346
            if (frame->height >= INT_MAX / 4 ||
196!
347
                    frame->width >= INT_MAX / 4 ||
98!
348
                    frame->height * frame->width * 4 >= INT_MAX) {
98✔
349
                sixel_helper_set_additional_message(
×
350
                    "load_with_coregraphics: too large image.");
351
                status = SIXEL_RUNTIME_ERROR;
×
352
                goto end;
×
353
            }
354

355
            stride = (size_t)frame->width * 4;
98✔
356
            sixel_frame_set_pixels(frame,
196✔
357
                                   sixel_allocator_malloc(
98✔
358
                                       pchunk->allocator,
98✔
359
                                       (size_t)(frame->height * stride)));
98✔
360
            pixels = sixel_frame_get_pixels(frame);
98✔
361
            if (pixels == NULL) {
98✔
362
                sixel_helper_set_additional_message(
×
363
                    "load_with_coregraphics: sixel_allocator_malloc()"
364
                    " failed.");
365
                status = SIXEL_BAD_ALLOCATION;
×
366
                goto end;
×
367
            }
368

369
            color_space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
98✔
370
            if (! color_space) {
98!
371
                CGImageRelease(image);
×
372
                goto end;
×
373
            }
374

375
            ctx = CGBitmapContextCreate(pixels,
196✔
376
                                        (size_t)frame->width,
98✔
377
                                        (size_t)frame->height,
98✔
378
                                        8,
379
                                        stride,
98✔
380
                                        color_space,
98✔
381
                                        kCGImageAlphaPremultipliedLast |
382
                                                kCGBitmapByteOrder32Big);
383
            if (!ctx) {
98✔
384
                CGImageRelease(image);
×
385
                goto end;
×
386
            }
387

388
            CGContextDrawImage(ctx,
196✔
389
                               CGRectMake(0, 0, frame->width, frame->height),
98✔
390
                               image);
98✔
391
            CGContextRelease(ctx);
98✔
392
            ctx = NULL;
98✔
393

394
            frame->multiframe = (!fstatic && frame_count > 1);
98✔
395
            status = fn_load(frame, context);
98✔
396
            CGImageRelease(image);
98✔
397
            image = NULL;
98✔
398
            if (status != SIXEL_OK) {
98✔
399
                goto end;
×
400
            }
401

402
            if (sixel_loader_callback_is_canceled(context)) {
98✔
403
                status = SIXEL_INTERRUPTED;
×
404
                goto end;
×
405
            }
406

407
            ++frame_index;
98✔
408
            ++frames_in_loop;
98✔
409

410
            if (fstatic) {
98✔
411
                status = SIXEL_OK;
21✔
412
                goto end;
21✔
413
            }
414
        }
415

416
        ++loop_no;
28✔
417
        stop_loop = 0;
28✔
418

419
        if (total_frames <= 1 || loop_control == SIXEL_LOOP_DISABLE) {
28!
420
            stop_loop = 1;
28✔
421
        } else if (loop_control == SIXEL_LOOP_AUTO) {
28!
422
            if (anim_loop_count < 0) {
×
423
                stop_loop = 1;
×
424
            } else if (anim_loop_count > 0 && loop_no >= anim_loop_count) {
×
425
                stop_loop = 1;
×
426
            }
×
427
        }
×
428

429
        if (stop_loop) {
28!
430
            break;
28✔
431
        }
432
    }
433

434
    status = SIXEL_OK;
28✔
435

436
end:
437
    if (ctx) {
126!
438
        CGContextRelease(ctx);
×
439
    }
×
440
    if (color_space) {
115✔
441
        CGColorSpaceRelease(color_space);
49✔
442
    }
49✔
443
    if (image) {
122!
444
        CGImageRelease(image);
×
445
    }
×
446
    if (source) {
126!
447
        CFRelease(source);
126✔
448
    }
126✔
449
    if (props) {
137✔
450
        CFRelease(props);
49✔
451
    }
49✔
452
    if (data) {
126!
453
        CFRelease(data);
126✔
454
    }
126✔
455
    if (frame) {
126!
456
        sixel_frame_unref(frame);
126✔
457
    }
126✔
458
    return status;
198✔
459
}
72✔
460

461
#endif  /* HAVE_COREGRAPHICS */
462

463
#if !HAVE_COREGRAPHICS
464
/*
465
 * Anchor a harmless symbol so the translation unit stays non-empty when
466
 * CoreGraphics is unavailable.
467
 */
468
typedef int loader_coregraphics_disabled;
469
#endif
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