FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/ktxdec.c
Date: 2026-10-06 20:08:58
Exec Total Coverage
Lines: 3 155 1.9%
Functions: 1 6 16.7%
Branches: 3 122 2.5%

Line Branch Exec 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 #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 {
67 ✗ int base = (e >= 0x93D0 && e <= 0x93DD) ? (e - 0x20) : e;
68 ✗ for (int i = 0; i < 14; i++) {
69 ✗ if (astc_gl_linear[i] == (int)base) {
70 ✗ *bx = astc_bx[i];
71 ✗ *by = astc_by[i];
72 ✗ return 1;
73 }
74 }
75 ✗ return 0;
76 }
77
78 8139 static int ktx_probe(const AVProbeData *p)
79 {
80 int bx, by;
81
82
3/4
✓ Branch 0 taken 8137 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 8137 times.
✗ Branch 3 not taken.
8139 if (p->buf_size < 32 || memcmp(p->buf, ktx_magic, sizeof(ktx_magic)))
83 8139 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 ✗ if (!gl_enum_to_block(AV_RL32(p->buf + 28), &bx, &by))
89 ✗ 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 ✗ return AVPROBE_SCORE_MAX - 1;
94 }
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 {
101 ✗ const char *p = value;
102
103 ✗ while (p && *p) {
104 ✗ if (p[0] == 'S' && p[1] == '=' && p[2])
105 ✗ *hflip = p[2] == 'l';
106 ✗ else if (p[0] == 'T' && p[1] == '=' && p[2])
107 ✗ *vflip = p[2] == 'u';
108 ✗ p = strchr(p, ',');
109 ✗ if (p)
110 ✗ p++;
111 }
112 ✗ }
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 {
119 ✗ AVIOContext *pb = s->pb;
120 uint8_t buf[256];
121 ✗ uint32_t left = kvdata;
122
123 ✗ *hflip = *vflip = 0;
124
125 ✗ while (left >= 4) {
126 ✗ uint32_t entry = avio_rl32(pb);
127 ✗ uint32_t padded = (entry + 3) & ~3u;
128
129 ✗ left -= 4;
130 ✗ if (!entry || entry > left || padded > left) {
131 ✗ av_log(s, AV_LOG_ERROR, "Invalid KTX key/value data.\n");
132 ✗ return AVERROR_INVALIDDATA;
133 }
134
135 ✗ if (entry >= sizeof(buf)) {
136 /* Nothing interesting is this large. */
137 ✗ if (avio_skip(pb, padded) < 0)
138 ✗ return AVERROR_INVALIDDATA;
139 } else {
140 const char *key, *value;
141
142 ✗ if (ffio_read_size(pb, buf, entry) < 0)
143 ✗ return AVERROR_INVALIDDATA;
144 ✗ buf[entry] = 0;
145
146 ✗ key = (const char *)buf;
147 ✗ value = key + strlen(key) + 1;
148 ✗ if (value < (const char *)buf + entry) {
149 ✗ if (!strcmp(key, "KTXorientation"))
150 ✗ ktx_orientation_flips(value, hflip, vflip);
151 else
152 ✗ av_log(s, AV_LOG_VERBOSE, "Ignoring KTX key '%s'.\n", key);
153 }
154 ✗ if (avio_skip(pb, padded - entry) < 0)
155 ✗ return AVERROR_INVALIDDATA;
156 }
157 ✗ left -= padded;
158 }
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 ✗ if (left) {
164 ✗ av_log(s, AV_LOG_ERROR, "Invalid KTX key/value data.\n");
165 ✗ return AVERROR_INVALIDDATA;
166 }
167
168 ✗ return 0;
169 }
170
171 ✗ static int ktx_read_header(AVFormatContext *s)
172 {
173 ✗ AVIOContext *pb = s->pb;
174 ✗ KTXDemuxerContext *ktx = s->priv_data;
175 AVStream *st;
176 uint8_t hdr[64];
177 uint32_t endian, gl_type, gl_type_size, gl_format, gl_internal;
178 uint32_t gl_base_internal, w, h, depth, array, faces, mips;
179 uint32_t kvdata, image_size;
180 uint8_t image_size_buf[4];
181 ✗ int bx = 0, by = 0;
182 ✗ int hflip = 0, vflip = 0;
183 uint8_t extra[16];
184
185 ✗ if (ffio_read_size(pb, hdr, sizeof(hdr)) < 0)
186 ✗ return AVERROR_INVALIDDATA;
187 ✗ if (memcmp(hdr, ktx_magic, sizeof(ktx_magic))) {
188 ✗ av_log(s, AV_LOG_ERROR, "Not a KTX file (bad magic).\n");
189 ✗ return AVERROR_INVALIDDATA;
190 }
191
192 ✗ endian = AV_RL32(hdr + 12);
193 ✗ gl_type = AV_RL32(hdr + 16);
194 ✗ gl_type_size = AV_RL32(hdr + 20);
195 ✗ gl_format = AV_RL32(hdr + 24);
196 ✗ gl_internal = AV_RL32(hdr + 28);
197 ✗ gl_base_internal = AV_RL32(hdr + 32);
198 ✗ w = AV_RL32(hdr + 36);
199 ✗ h = AV_RL32(hdr + 40);
200 ✗ depth = AV_RL32(hdr + 44);
201 ✗ array = AV_RL32(hdr + 48);
202 ✗ faces = AV_RL32(hdr + 52);
203 ✗ mips = AV_RL32(hdr + 56);
204 ✗ kvdata = AV_RL32(hdr + 60);
205
206 ✗ if (endian != 0x04030201) {
207 ✗ av_log(s, AV_LOG_ERROR, "KTX big-endian / unknown endianness not supported.\n");
208 ✗ return AVERROR_INVALIDDATA;
209 }
210 ✗ if (gl_type || gl_type_size != 1 || gl_format || gl_base_internal != 0x1908 ||
211 ✗ !w || !h || w > 0xFFFFFF || h > 0xFFFFFF ||
212 ✗ depth || array || faces != 1 || mips != 1) {
213 ✗ av_log(s, AV_LOG_ERROR, "Unsupported KTX texture layout.\n");
214 ✗ return AVERROR_INVALIDDATA;
215 }
216
217 ✗ if (!gl_enum_to_block(gl_internal, &bx, &by)) {
218 ✗ av_log(s, AV_LOG_ERROR, "Unsupported ASTC GL format 0x%X in KTX.\n", gl_internal);
219 ✗ return AVERROR_INVALIDDATA;
220 }
221
222 ✗ if (ktx_read_orientation(s, kvdata, &hflip, &vflip) < 0)
223 ✗ return AVERROR_INVALIDDATA;
224 ✗ if (ffio_read_size(pb, image_size_buf, sizeof(image_size_buf)) < 0)
225 ✗ return AVERROR_INVALIDDATA;
226 ✗ image_size = AV_RL32(image_size_buf);
227 ✗ if (!image_size || image_size > INT_MAX)
228 ✗ 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 {
233 ✗ uint64_t expected = (((uint64_t)w + bx - 1) / bx) *
234 ✗ (((uint64_t)h + by - 1) / by) * 16;
235 ✗ if (image_size != expected) {
236 ✗ av_log(s, AV_LOG_ERROR,
237 "KTX image size %u does not match the %"PRIu64" bytes "
238 "required for a %ux%u texture with %dx%d blocks.\n",
239 image_size, expected, w, h, bx, by);
240 ✗ return AVERROR_INVALIDDATA;
241 }
242 }
243 ✗ ktx->image_size = image_size;
244
245 ✗ st = avformat_new_stream(s, NULL);
246 ✗ if (!st)
247 ✗ return AVERROR(ENOMEM);
248
249 ✗ st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
250 ✗ st->codecpar->codec_id = AV_CODEC_ID_ASTC;
251 ✗ st->codecpar->width = w;
252 ✗ 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 ✗ st->codecpar->profile = gl_internal >= 0x93D0 && gl_internal <= 0x93DD ?
260 ✗ AV_PROFILE_ASTC_LDR_SRGB : AV_PROFILE_ASTC_LINEAR_ANY;
261 ✗ st->codecpar->format = st->codecpar->profile == AV_PROFILE_ASTC_LDR_SRGB ?
262 ✗ 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 ✗ memset(extra, 0, sizeof(extra));
267 ✗ memcpy(extra, astc_magic, sizeof(astc_magic));
268 ✗ extra[4] = (uint8_t)bx;
269 ✗ extra[5] = (uint8_t)by;
270 ✗ extra[6] = 1;
271 ✗ AV_WL24(extra + 7, w);
272 ✗ AV_WL24(extra + 10, h);
273 ✗ extra[13] = 1;
274 ✗ if (ff_alloc_extradata(st->codecpar, ASTC_HEADER_SIZE) < 0)
275 ✗ return AVERROR(ENOMEM);
276 ✗ 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 ✗ if (hflip || vflip) {
281 AVPacketSideData *sd;
282 int32_t *matrix;
283
284 ✗ sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
285 ✗ &st->codecpar->nb_coded_side_data,
286 AV_PKT_DATA_DISPLAYMATRIX,
287 9 * sizeof(*matrix), 0);
288 ✗ if (!sd)
289 ✗ return AVERROR(ENOMEM);
290 ✗ matrix = (int32_t *)sd->data;
291 ✗ av_display_rotation_set(matrix, 0);
292 ✗ av_display_matrix_flip(matrix, hflip, vflip);
293 ✗ av_log(s, AV_LOG_VERBOSE,
294 "Applying KTXorientation (hflip %d, vflip %d).\n", hflip, vflip);
295 }
296
297 ✗ avpriv_set_pts_info(st, 64, 1, 1);
298 ✗ return 0;
299 }
300
301 ✗ static int ktx_read_packet(AVFormatContext *s, AVPacket *pkt)
302 {
303 ✗ KTXDemuxerContext *ktx = s->priv_data;
304 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 ✗ if (ktx->truncated)
310 ✗ return AVERROR_INVALIDDATA;
311
312 ✗ if (!ktx->image_size)
313 ✗ 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 ✗ ret = av_get_packet(s->pb, pkt, ktx->image_size);
319 ✗ if (ret == AVERROR_EOF)
320 ✗ ret = 0;
321 ✗ if (ret < 0)
322 ✗ return ret;
323 ✗ if (ret != ktx->image_size) {
324 ✗ av_log(s, AV_LOG_ERROR,
325 "Truncated KTX image: got %d of %u bytes.\n",
326 ret, ktx->image_size);
327 ✗ av_packet_unref(pkt);
328 ✗ ktx->truncated = 1;
329 ✗ return AVERROR_INVALIDDATA;
330 }
331
332 ✗ pkt->stream_index = 0;
333 ✗ pkt->flags |= AV_PKT_FLAG_KEY;
334 ✗ ktx->image_size = 0; /* single mip */
335 ✗ return 0;
336 }
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 };
349