FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/ktxdec.c
Date: 2026-09-15 09:36:26
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 8043 static int ktx_probe(const AVProbeData *p)
79 {
80 int bx, by;
81
82
3/4
✓ Branch 0 taken 8042 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 8042 times.
✗ Branch 3 not taken.
8043 if (p->buf_size < 32 || memcmp(p->buf, ktx_magic, sizeof(ktx_magic)))
83 8043 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