/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 | | }; |