Coverage Report

Created: 2026-10-28 08:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ffmpeg/libavformat/ktxdec.c
Line
Count
Source
1
/*
2
 * KTX 1.0 demuxer for ASTC
3
 * Copyright (c) 2026 Jun Zhao
4
 *
5
 * This file is part of FFmpeg.
6
 *
7
 * FFmpeg is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU Lesser General Public
9
 * License as published by the Free Software Foundation; either
10
 * version 2.1 of the License, or (at your option) any later version.
11
 *
12
 * FFmpeg is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15
 * Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public
18
 * License along with FFmpeg; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20
 */
21
22
/**
23
 * @file
24
 * KTX 1.0 demuxer for ASTC textures.
25
 *
26
 * Parses the KTX 1.0 header, recovers the ASTC block size from the GL
27
 * internal format enum, and synthesizes a 16-byte .astc-style extradata
28
 * for the decoder. The raw ASTC bitstream is emitted as a single packet.
29
 */
30
31
#include "config_components.h"
32
33
#include <inttypes.h>
34
#include <limits.h>
35
36
#include "libavcodec/defs.h"
37
#include "avformat.h"
38
#include "avio.h"
39
#include "avio_internal.h"
40
#include "demux.h"
41
#include "internal.h"
42
#include "libavutil/display.h"
43
#include "libavutil/intreadwrite.h"
44
45
890
#define ASTC_HEADER_SIZE 16
46
static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C };
47
static const uint8_t ktx_magic[12] = {
48
    0xAB, 'K', 'T', 'X', ' ', '1', '1', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
49
};
50
51
/* ASTC GL internal format enums (2D), linear base 0x93B0, sRGB = +0x20. */
52
static const int astc_gl_linear[14] = {
53
    0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7,
54
    0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD
55
};
56
static const int astc_bx[14] = { 4, 5, 5, 6, 6, 8, 8, 8, 10, 10, 10, 10, 12, 12 };
57
static const int astc_by[14] = { 4, 4, 5, 5, 6, 5, 6, 8, 5, 6, 8, 10, 10, 12 };
58
59
typedef struct KTXDemuxerContext {
60
    uint32_t image_size;
61
    int      truncated;
62
} KTXDemuxerContext;
63
64
/* Map a GL internal format enum to a 2D ASTC block size. Returns 1 on success. */
65
static int gl_enum_to_block(uint32_t e, int *bx, int *by)
66
1.40k
{
67
1.40k
    int base = (e >= 0x93D0 && e <= 0x93DD) ? (e - 0x20) : e;
68
8.90k
    for (int i = 0; i < 14; i++) {
69
8.66k
        if (astc_gl_linear[i] == (int)base) {
70
1.16k
            *bx = astc_bx[i];
71
1.16k
            *by = astc_by[i];
72
1.16k
            return 1;
73
1.16k
        }
74
8.66k
    }
75
242
    return 0;
76
1.40k
}
77
78
static int ktx_probe(const AVProbeData *p)
79
963k
{
80
963k
    int bx, by;
81
82
963k
    if (p->buf_size < 32 || memcmp(p->buf, ktx_magic, sizeof(ktx_magic)))
83
963k
        return 0;
84
85
    /* KTX 1.0 carries ETC, uncompressed and other payloads this demuxer cannot
86
     * read, so only claim the file once glInternalFormat is an ASTC format we
87
     * actually support. */
88
193
    if (!gl_enum_to_block(AV_RL32(p->buf + 28), &bx, &by))
89
193
        return 0;
90
91
    /* Just below the maximum: this is a KTX file, but only the ASTC subset of
92
     * the container is implemented here. */
93
0
    return AVPROBE_SCORE_MAX - 1;
94
193
}
95
96
/* Map a KTXorientation value such as "S=r,T=d" onto the flips needed to
97
 * bring the stored rows back to the top-down, left-to-right order the
98
 * decoder produces. The R component is irrelevant for 2D textures. */
99
static void ktx_orientation_flips(const char *value, int *hflip, int *vflip)
100
1.78k
{
101
1.78k
    const char *p = value;
102
103
9.03k
    while (p && *p) {
104
7.24k
        if (p[0] == 'S' && p[1] == '=' && p[2])
105
275
            *hflip = p[2] == 'l';
106
6.97k
        else if (p[0] == 'T' && p[1] == '=' && p[2])
107
319
            *vflip = p[2] == 'u';
108
7.24k
        p = strchr(p, ',');
109
7.24k
        if (p)
110
5.47k
            p++;
111
7.24k
    }
112
1.78k
}
113
114
/* Walk the key/value block, picking up KTXorientation. Unknown or oversized
115
 * entries are skipped rather than buffered wholesale. */
116
static int ktx_read_orientation(AVFormatContext *s, uint32_t kvdata,
117
                                int *hflip, int *vflip)
118
1.16k
{
119
1.16k
    AVIOContext *pb = s->pb;
120
1.16k
    uint8_t buf[256];
121
1.16k
    uint32_t left = kvdata;
122
123
1.16k
    *hflip = *vflip = 0;
124
125
6.26k
    while (left >= 4) {
126
5.70k
        uint32_t entry = avio_rl32(pb);
127
5.70k
        uint32_t padded = (entry + 3) & ~3u;
128
129
5.70k
        left -= 4;
130
5.70k
        if (!entry || entry > left || padded > left) {
131
194
            av_log(s, AV_LOG_ERROR, "Invalid KTX key/value data.\n");
132
194
            return AVERROR_INVALIDDATA;
133
194
        }
134
135
5.51k
        if (entry >= sizeof(buf)) {
136
            /* Nothing interesting is this large. */
137
1.05k
            if (avio_skip(pb, padded) < 0)
138
201
                return AVERROR_INVALIDDATA;
139
4.45k
        } else {
140
4.45k
            const char *key, *value;
141
142
4.45k
            if (ffio_read_size(pb, buf, entry) < 0)
143
80
                return AVERROR_INVALIDDATA;
144
4.37k
            buf[entry] = 0;
145
146
4.37k
            key = (const char *)buf;
147
4.37k
            value = key + strlen(key) + 1;
148
4.37k
            if (value < (const char *)buf + entry) {
149
2.64k
                if (!strcmp(key, "KTXorientation"))
150
1.78k
                    ktx_orientation_flips(value, hflip, vflip);
151
859
                else
152
859
                    av_log(s, AV_LOG_VERBOSE, "Ignoring KTX key '%s'.\n", key);
153
2.64k
            }
154
4.37k
            if (avio_skip(pb, padded - entry) < 0)
155
134
                return AVERROR_INVALIDDATA;
156
4.37k
        }
157
5.09k
        left -= padded;
158
5.09k
    }
159
160
    /* Every entry occupies a padded, four byte aligned slot, so a leftover
161
     * tail means the declared block size is not aligned and the image payload
162
     * does not start where the header claims it does. */
163
555
    if (left) {
164
2
        av_log(s, AV_LOG_ERROR, "Invalid KTX key/value data.\n");
165
2
        return AVERROR_INVALIDDATA;
166
2
    }
167
168
553
    return 0;
169
555
}
170
171
static int ktx_read_header(AVFormatContext *s)
172
1.90k
{
173
1.90k
    AVIOContext *pb = s->pb;
174
1.90k
    KTXDemuxerContext *ktx = s->priv_data;
175
1.90k
    AVStream *st;
176
1.90k
    uint8_t hdr[64];
177
1.90k
    uint32_t endian, gl_type, gl_type_size, gl_format, gl_internal;
178
1.90k
    uint32_t gl_base_internal, w, h, depth, array, faces, mips;
179
1.90k
    uint32_t kvdata, image_size;
180
1.90k
    uint8_t image_size_buf[4];
181
1.90k
    int bx = 0, by = 0;
182
1.90k
    int hflip = 0, vflip = 0;
183
1.90k
    uint8_t extra[16];
184
185
1.90k
    if (ffio_read_size(pb, hdr, sizeof(hdr)) < 0)
186
130
        return AVERROR_INVALIDDATA;
187
1.77k
    if (memcmp(hdr, ktx_magic, sizeof(ktx_magic))) {
188
143
        av_log(s, AV_LOG_ERROR, "Not a KTX file (bad magic).\n");
189
143
        return AVERROR_INVALIDDATA;
190
143
    }
191
192
1.63k
    endian          = AV_RL32(hdr + 12);
193
1.63k
    gl_type         = AV_RL32(hdr + 16);
194
1.63k
    gl_type_size    = AV_RL32(hdr + 20);
195
1.63k
    gl_format       = AV_RL32(hdr + 24);
196
1.63k
    gl_internal     = AV_RL32(hdr + 28);
197
1.63k
    gl_base_internal = AV_RL32(hdr + 32);
198
1.63k
    w               = AV_RL32(hdr + 36);
199
1.63k
    h               = AV_RL32(hdr + 40);
200
1.63k
    depth           = AV_RL32(hdr + 44);
201
1.63k
    array           = AV_RL32(hdr + 48);
202
1.63k
    faces           = AV_RL32(hdr + 52);
203
1.63k
    mips            = AV_RL32(hdr + 56);
204
1.63k
    kvdata          = AV_RL32(hdr + 60);
205
206
1.63k
    if (endian != 0x04030201) {
207
46
        av_log(s, AV_LOG_ERROR, "KTX big-endian / unknown endianness not supported.\n");
208
46
        return AVERROR_INVALIDDATA;
209
46
    }
210
1.58k
    if (gl_type || gl_type_size != 1 || gl_format || gl_base_internal != 0x1908 ||
211
1.43k
        !w || !h || w > 0xFFFFFF || h > 0xFFFFFF ||
212
1.37k
        depth || array || faces != 1 || mips != 1) {
213
374
        av_log(s, AV_LOG_ERROR, "Unsupported KTX texture layout.\n");
214
374
        return AVERROR_INVALIDDATA;
215
374
    }
216
217
1.21k
    if (!gl_enum_to_block(gl_internal, &bx, &by)) {
218
49
        av_log(s, AV_LOG_ERROR, "Unsupported ASTC GL format 0x%X in KTX.\n", gl_internal);
219
49
        return AVERROR_INVALIDDATA;
220
49
    }
221
222
1.16k
    if (ktx_read_orientation(s, kvdata, &hflip, &vflip) < 0)
223
611
        return AVERROR_INVALIDDATA;
224
553
    if (ffio_read_size(pb, image_size_buf, sizeof(image_size_buf)) < 0)
225
6
        return AVERROR_INVALIDDATA;
226
547
    image_size = AV_RL32(image_size_buf);
227
547
    if (!image_size || image_size > INT_MAX)
228
34
        return AVERROR_INVALIDDATA;
229
    /* The payload must hold exactly the blocks the texture geometry needs;
230
     * anything else would either truncate the image or smuggle in trailing
231
     * data that the decoder would ignore. */
232
513
    {
233
513
        uint64_t expected = (((uint64_t)w + bx - 1) / bx) *
234
513
                            (((uint64_t)h + by - 1) / by) * 16;
235
513
        if (image_size != expected) {
236
68
            av_log(s, AV_LOG_ERROR,
237
68
                   "KTX image size %u does not match the %"PRIu64" bytes "
238
68
                   "required for a %ux%u texture with %dx%d blocks.\n",
239
68
                   image_size, expected, w, h, bx, by);
240
68
            return AVERROR_INVALIDDATA;
241
68
        }
242
513
    }
243
445
    ktx->image_size = image_size;
244
245
445
    st = avformat_new_stream(s, NULL);
246
445
    if (!st)
247
0
        return AVERROR(ENOMEM);
248
249
445
    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
250
445
    st->codecpar->codec_id   = AV_CODEC_ID_ASTC;
251
445
    st->codecpar->width      = w;
252
445
    st->codecpar->height     = h;
253
    /* The GL internal format fixes the colour space but not the endpoint
254
     * format: an sRGB texture is LDR, while a linear one may hold LDR, HDR RGB
255
     * with LDR alpha, or full HDR endpoints. Report that distinction so the
256
     * decoder does not assume LDR for a linear texture; a linear texture is
257
     * sampled with HDR precision by default (LINEAR_ANY), so the stream format
258
     * is the matching half-float one rather than 8-bit RGBA. */
259
445
    st->codecpar->profile    = gl_internal >= 0x93D0 && gl_internal <= 0x93DD ?
260
405
                               AV_PROFILE_ASTC_LDR_SRGB : AV_PROFILE_ASTC_LINEAR_ANY;
261
445
    st->codecpar->format     = st->codecpar->profile == AV_PROFILE_ASTC_LDR_SRGB ?
262
445
                               AV_PIX_FMT_RGBA : AV_PIX_FMT_RGBAF16;
263
264
    /* Synthesize the .astc-style extradata for the decoder. Bytes [13-15] hold
265
     * the image depth (dim_z), which is always 1 here: KTX 1.0 2D only. */
266
445
    memset(extra, 0, sizeof(extra));
267
445
    memcpy(extra, astc_magic, sizeof(astc_magic));
268
445
    extra[4] = (uint8_t)bx;
269
445
    extra[5] = (uint8_t)by;
270
445
    extra[6] = 1;
271
445
    AV_WL24(extra + 7,  w);
272
445
    AV_WL24(extra + 10, h);
273
445
    extra[13] = 1;
274
445
    if (ff_alloc_extradata(st->codecpar, ASTC_HEADER_SIZE) < 0)
275
0
        return AVERROR(ENOMEM);
276
445
    memcpy(st->codecpar->extradata, extra, ASTC_HEADER_SIZE);
277
278
    /* Carry the stored row order to the caller instead of silently returning
279
     * a mirrored image. */
280
445
    if (hflip || vflip) {
281
0
        AVPacketSideData *sd;
282
0
        int32_t *matrix;
283
284
0
        sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
285
0
                                     &st->codecpar->nb_coded_side_data,
286
0
                                     AV_PKT_DATA_DISPLAYMATRIX,
287
0
                                     9 * sizeof(*matrix), 0);
288
0
        if (!sd)
289
0
            return AVERROR(ENOMEM);
290
0
        matrix = (int32_t *)sd->data;
291
0
        av_display_rotation_set(matrix, 0);
292
0
        av_display_matrix_flip(matrix, hflip, vflip);
293
0
        av_log(s, AV_LOG_VERBOSE,
294
0
               "Applying KTXorientation (hflip %d, vflip %d).\n", hflip, vflip);
295
0
    }
296
297
445
    avpriv_set_pts_info(st, 64, 1, 1);
298
445
    return 0;
299
445
}
300
301
static int ktx_read_packet(AVFormatContext *s, AVPacket *pkt)
302
770
{
303
770
    KTXDemuxerContext *ktx = s->priv_data;
304
770
    int ret;
305
306
    /* Keep reporting a detected truncation instead of falling through to EOF:
307
     * a caller that stops at the first error must not see the broken image as
308
     * a clean end of stream. */
309
770
    if (ktx->truncated)
310
313
        return AVERROR_INVALIDDATA;
311
312
457
    if (!ktx->image_size)
313
12
        return AVERROR_EOF;
314
315
    /* A payload that is missing entirely reads as EOF, which has to be
316
     * reported as the truncation it is rather than as the end of a valid
317
     * stream. */
318
445
    ret = av_get_packet(s->pb, pkt, ktx->image_size);
319
445
    if (ret == AVERROR_EOF)
320
20
        ret = 0;
321
445
    if (ret < 0)
322
0
        return ret;
323
445
    if (ret != ktx->image_size) {
324
438
        av_log(s, AV_LOG_ERROR,
325
438
               "Truncated KTX image: got %d of %u bytes.\n",
326
438
               ret, ktx->image_size);
327
438
        av_packet_unref(pkt);
328
438
        ktx->truncated = 1;
329
438
        return AVERROR_INVALIDDATA;
330
438
    }
331
332
7
    pkt->stream_index = 0;
333
7
    pkt->flags |= AV_PKT_FLAG_KEY;
334
7
    ktx->image_size = 0; /* single mip */
335
7
    return 0;
336
445
}
337
338
const FFInputFormat ff_ktx_demuxer = {
339
    .p.name         = "ktx",
340
    .p.long_name    = NULL_IF_CONFIG_SMALL("KTX 1.0 (Khronos Texture) for ASTC"),
341
    .p.mime_type    = "image/ktx",
342
    .p.extensions   = "ktx",
343
    .p.flags        = AVFMT_NOTIMESTAMPS,
344
    .priv_data_size = sizeof(KTXDemuxerContext),
345
    .read_probe     = ktx_probe,
346
    .read_header    = ktx_read_header,
347
    .read_packet    = ktx_read_packet,
348
};