Coverage Report

Created: 2026-02-03 07:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libass/libass/ass_drawing.c
Line
Count
Source
1
/*
2
 * Copyright (C) 2009 Grigori Goronzy <greg@geekmind.org>
3
 *
4
 * This file is part of libass.
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 */
18
19
#include "config.h"
20
#include "ass_compat.h"
21
22
#include <math.h>
23
#include <stdbool.h>
24
#include <limits.h>
25
26
#include "ass_utils.h"
27
#include "ass_drawing.h"
28
#include "ass_font.h"
29
30
129
#define DRAWING_INITIAL_POINTS 100
31
129
#define DRAWING_INITIAL_SEGMENTS 100
32
33
/*
34
 * \brief Free a list of tokens
35
 */
36
static void drawing_free_tokens(ASS_DrawingToken *token)
37
129
{
38
129
    while (token) {
39
0
        ASS_DrawingToken *at = token;
40
0
        token = token->next;
41
0
        free(at);
42
0
    }
43
129
}
44
45
static inline bool add_node(ASS_DrawingToken **tail, ASS_TokenType type, ASS_Vector point)
46
0
{
47
0
    assert(tail && *tail);
48
49
0
    ASS_DrawingToken *new_tail = malloc(sizeof(**tail));
50
0
    if (!new_tail)
51
0
        return false;
52
0
    (*tail)->next = new_tail;
53
0
    new_tail->prev = *tail;
54
0
    new_tail->next = NULL;
55
0
    new_tail->type = type;
56
0
    new_tail->point = point;
57
0
    *tail = new_tail;
58
0
    return true;
59
0
}
60
61
static inline bool get_point(const char **str, ASS_Vector *point)
62
6
{
63
6
    double x, y;
64
6
    if (!mystrtod((char **) str, &x) || !mystrtod((char **) str, &y))
65
6
        return false;
66
0
    *point = (ASS_Vector) {double_to_d6(x), double_to_d6(y)};
67
0
    return true;
68
6
}
69
70
71
/**
72
 * Parses and advances the string for exactly 3 points.
73
 * If the string contains fewer than 3 points,
74
 * any initial matching coordinates are still consumed.
75
 * If an allocation fails, error will be set to true.
76
 * \return whether three valid points were added
77
 */
78
static bool add_3_points(const char **str, ASS_DrawingToken **tail, ASS_TokenType type, bool *error)
79
0
{
80
0
    ASS_Vector buf[3];
81
82
0
    if (!*str)
83
0
        return false;
84
85
0
    bool valid = get_point(str, buf + 0);
86
0
    valid = valid && get_point(str, buf + 1);
87
0
    valid = valid && get_point(str, buf + 2);
88
89
0
    if (!valid)
90
0
        return false;
91
92
0
    valid = add_node(tail, type, buf[0]);
93
0
    valid = valid && add_node(tail, type, buf[1]);
94
0
    valid = valid && add_node(tail, type, buf[2]);
95
0
    if (!valid) {
96
0
        *error = true;
97
0
        return false;
98
0
    }
99
100
0
    return true;
101
0
}
102
103
/*
104
 * Parses and advances the string while it matches points.
105
 * Each set of batch_size points will be turned into tokens and appended to tail.
106
 * Partial matches (i.e. an insufficient amount of coordinates) are still consumed.
107
 * If an allocation fails, error will be set to true.
108
 * \return count of added points
109
 */
110
static size_t add_many_points(const char **str, ASS_DrawingToken **tail,
111
                              ASS_TokenType type, size_t batch_size, bool *error)
112
0
{
113
0
    ASS_Vector buf[3];
114
0
    assert(batch_size <= (sizeof(buf) / sizeof(*buf)));
115
116
0
    if (!*str)
117
0
        return 0;
118
119
0
    size_t count_total = 0;
120
0
    size_t count_batch = 0;
121
0
    while (**str) {
122
0
        ASS_Vector point;
123
0
        if (!get_point(str, &point))
124
0
            break;
125
0
        buf[count_batch] = point;
126
0
        count_total++;
127
0
        count_batch++;
128
129
0
        if (count_batch != batch_size)
130
0
            continue;
131
132
0
        for (size_t i = 0; i < count_batch; i++)
133
0
            if (!add_node(tail, type, buf[i])) {
134
0
                *error = true;
135
0
                return count_total - count_batch + i;
136
0
            }
137
0
        count_batch = 0;
138
0
    }
139
140
0
    return count_total - count_batch;
141
0
}
142
143
static inline bool add_root_node(ASS_DrawingToken **root, ASS_DrawingToken **tail,
144
                                 size_t *points, ASS_Vector point, ASS_TokenType type)
145
0
{
146
0
    *root = *tail = calloc(1, sizeof(ASS_DrawingToken));
147
0
    if (!*root)
148
0
        return false;
149
0
    (*root)->point = point;
150
0
    (*root)->type = type;
151
0
    *points = 1;
152
0
    return true;
153
0
}
154
155
/*
156
 * \brief Tokenize a drawing string into a list of ASS_DrawingToken
157
 * This also expands points for closing b-splines
158
 */
159
static ASS_DrawingToken *drawing_tokenize(const char *str)
160
129
{
161
129
    const char *p = str;
162
129
    ASS_DrawingToken *root = NULL, *tail = NULL, *spline_start = NULL;
163
129
    size_t points = 0;
164
129
    bool m_seen = false;
165
129
    bool error = false;
166
167
634
    while (p && *p) {
168
505
        char cmd = *p;
169
505
        p++;
170
        /* VSFilter compat:
171
         * In guliverkli(2) VSFilter all drawings
172
         * whose first known (but potentially invalid) command isn't m are rejected.
173
         * xy-VSF and MPC-HC ISR later relaxed this (possibly inadvertenly),
174
         * such that all known commands but n are ignored if there was no prior node yet.
175
         * If an invalid m preceded n, the latter becomes the root node, otherwise
176
         * if n comes before any other not-ignored command the entire drawing is rejected.
177
         * 'p' is further restricted and ignored unless there are already >= 3 nodes.
178
         * This relaxation was a byproduct of a fix for crashing on drawings
179
         * containing commands with fewer preceding nodes than expected.
180
         */
181
505
        switch (cmd) {
182
4
        case 'm':
183
4
            m_seen = true;
184
4
            if (!root) {
185
4
                ASS_Vector point;
186
4
                if (!get_point(&p, &point))
187
4
                    continue;
188
0
                if (!add_root_node(&root, &tail, &points, point, TOKEN_MOVE))
189
0
                    return NULL;
190
0
            }
191
0
            points += add_many_points(&p, &tail, TOKEN_MOVE, 1, &error);
192
0
            break;
193
2
        case 'n':
194
2
            if (!root) {
195
2
                ASS_Vector point;
196
2
                if (!get_point(&p, &point))
197
2
                    continue;
198
0
                if (!m_seen)
199
0
                    return NULL;
200
0
                if (!add_root_node(&root, &tail, &points, point, TOKEN_MOVE_NC))
201
0
                    return NULL;
202
0
            }
203
0
            points += add_many_points(&p, &tail, TOKEN_MOVE_NC, 1, &error);
204
0
            break;
205
6
        case 'l':
206
6
            if (!root)
207
6
                continue;
208
0
            points += add_many_points(&p, &tail, TOKEN_LINE, 1, &error);
209
0
            break;
210
0
        case 'b':
211
0
            if (!root)
212
0
                continue;
213
0
            points += add_many_points(&p, &tail, TOKEN_CUBIC_BEZIER, 3, &error);
214
0
            break;
215
6
        case 's':
216
6
            if (!root)
217
6
                continue;
218
            // Only the initial 3 points are TOKEN_B_SPLINE,
219
            // all following ones are TOKEN_EXTEND_SPLINE
220
0
            spline_start = tail;
221
0
            if (!add_3_points(&p, &tail, TOKEN_B_SPLINE, &error)) {
222
0
                spline_start = NULL;
223
0
                break;
224
0
            }
225
0
            points += 3;
226
            //-fallthrough
227
2
        case 'p':
228
2
            if (points < 3)
229
2
                continue;
230
0
            points += add_many_points(&p, &tail, TOKEN_EXTEND_SPLINE, 1, &error);
231
0
            break;
232
2
        case 'c':
233
2
            if (!spline_start)
234
2
                continue;
235
            // Close b-splines: add the first three points of the b-spline back to the end
236
0
            for (int i = 0; i < 3; i++) {
237
0
                if (!add_node(&tail, TOKEN_EXTEND_SPLINE, spline_start->point)) {
238
0
                    error = true;
239
0
                    break;
240
0
                }
241
0
                spline_start = spline_start->next;
242
0
            }
243
0
            spline_start = NULL;
244
0
            break;
245
483
        default:
246
            // Ignore, just search for next valid command
247
483
            break;
248
505
        }
249
483
        if (error)
250
0
            goto fail;
251
483
    }
252
253
129
    return root;
254
255
0
fail:
256
0
    drawing_free_tokens(root);
257
0
    return NULL;
258
129
}
259
260
/*
261
 * \brief Add curve to drawing
262
 */
263
static bool drawing_add_curve(ASS_Outline *outline, ASS_Rect *cbox,
264
                              ASS_DrawingToken *token, bool spline, int started)
265
0
{
266
0
    ASS_Vector p[4];
267
0
    for (int i = 0; i < 4; ++i) {
268
0
        p[i] = token->point;
269
0
        rectangle_update(cbox, p[i].x, p[i].y, p[i].x, p[i].y);
270
0
        token = token->next;
271
0
    }
272
273
0
    if (spline) {
274
0
        int x01 = (p[1].x - p[0].x) / 3;
275
0
        int y01 = (p[1].y - p[0].y) / 3;
276
0
        int x12 = (p[2].x - p[1].x) / 3;
277
0
        int y12 = (p[2].y - p[1].y) / 3;
278
0
        int x23 = (p[3].x - p[2].x) / 3;
279
0
        int y23 = (p[3].y - p[2].y) / 3;
280
281
0
        p[0].x = p[1].x + ((x12 - x01) >> 1);
282
0
        p[0].y = p[1].y + ((y12 - y01) >> 1);
283
0
        p[3].x = p[2].x + ((x23 - x12) >> 1);
284
0
        p[3].y = p[2].y + ((y23 - y12) >> 1);
285
0
        p[1].x += x12;
286
0
        p[1].y += y12;
287
0
        p[2].x -= x12;
288
0
        p[2].y -= y12;
289
0
    }
290
291
0
    return (started || ass_outline_add_point(outline, p[0], 0)) &&
292
0
        ass_outline_add_point(outline, p[1], 0) &&
293
0
        ass_outline_add_point(outline, p[2], 0) &&
294
0
        ass_outline_add_point(outline, p[3], OUTLINE_CUBIC_SPLINE);
295
0
}
296
297
/*
298
 * Anything produced by our tokenizer is already supposed to fulfill these requirements
299
 * where relevant, but let's check with an assert in builds without NDEBUG
300
 */
301
static inline void assert_3_forward(ASS_DrawingToken *token)
302
0
{
303
0
    assert(token && token->prev && token->next && token->next->next);
304
0
}
305
306
static inline void assert_4_back(ASS_DrawingToken *token)
307
0
{
308
0
    assert(token && token->prev && token->prev->prev && token->prev->prev->prev);
309
0
}
310
311
/*
312
 * \brief Convert token list to outline.  Calls the line and curve evaluators.
313
 */
314
bool ass_drawing_parse(ASS_Outline *outline, ASS_Rect *cbox,
315
                       const char *text, ASS_Library *lib)
316
129
{
317
129
    if (!ass_outline_alloc(outline, DRAWING_INITIAL_POINTS, DRAWING_INITIAL_SEGMENTS))
318
0
        return false;
319
129
    rectangle_reset(cbox);
320
321
129
    ASS_DrawingToken *tokens = drawing_tokenize(text);
322
323
129
    bool started = false;
324
129
    ASS_Vector pen = {0, 0};
325
129
    ASS_DrawingToken *token = tokens;
326
129
    while (token) {
327
        // Draw something according to current command
328
0
        switch (token->type) {
329
0
        case TOKEN_MOVE_NC:
330
0
            pen = token->point;
331
0
            rectangle_update(cbox, pen.x, pen.y, pen.x, pen.y);
332
0
            token = token->next;
333
0
            break;
334
0
        case TOKEN_MOVE:
335
0
            pen = token->point;
336
0
            rectangle_update(cbox, pen.x, pen.y, pen.x, pen.y);
337
0
            if (started) {
338
0
                if (!ass_outline_add_segment(outline, OUTLINE_LINE_SEGMENT))
339
0
                    goto error;
340
0
                ass_outline_close_contour(outline);
341
0
                started = false;
342
0
            }
343
0
            token = token->next;
344
0
            break;
345
0
        case TOKEN_LINE: {
346
0
            ASS_Vector to = token->point;
347
0
            rectangle_update(cbox, to.x, to.y, to.x, to.y);
348
0
            if (!started && !ass_outline_add_point(outline, pen, 0))
349
0
                goto error;
350
0
            if (!ass_outline_add_point(outline, to, OUTLINE_LINE_SEGMENT))
351
0
                goto error;
352
0
            started = true;
353
0
            token = token->next;
354
0
            break;
355
0
        }
356
0
        case TOKEN_CUBIC_BEZIER:
357
0
            assert_3_forward(token);
358
0
            if (!drawing_add_curve(outline, cbox, token->prev, false, started))
359
0
                goto error;
360
0
            token = token->next;
361
0
            token = token->next;
362
0
            token = token->next;
363
0
            started = true;
364
0
            break;
365
0
        case TOKEN_B_SPLINE:
366
0
            assert_3_forward(token);
367
0
            if (!drawing_add_curve(outline, cbox, token->prev, true, started))
368
0
                goto error;
369
0
            token = token->next;
370
0
            token = token->next;
371
0
            token = token->next;
372
0
            started = true;
373
0
            break;
374
0
        case TOKEN_EXTEND_SPLINE:
375
0
            assert_4_back(token);
376
0
            if (!drawing_add_curve(outline, cbox, token->prev->prev->prev, true, started))
377
0
                goto error;
378
0
            token = token->next;
379
0
            started = true;
380
0
            break;
381
0
        }
382
0
    }
383
384
    // Close the last contour
385
129
    if (started) {
386
0
        if (!ass_outline_add_segment(outline, OUTLINE_LINE_SEGMENT))
387
0
            goto error;
388
0
        ass_outline_close_contour(outline);
389
0
    }
390
391
129
    if (lib)
392
129
        ass_msg(lib, MSGL_V,
393
129
                "Parsed drawing with %zu points and %zu segments",
394
129
                outline->n_points, outline->n_segments);
395
396
129
    drawing_free_tokens(tokens);
397
129
    return true;
398
399
0
error:
400
0
    drawing_free_tokens(tokens);
401
0
    ass_outline_free(outline);
402
    return false;
403
129
}