This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit 7afd90fa9593dd1435d1c0eb08d46f7f8c5ef2d3
Author:     Jun Zhao <[email protected]>
AuthorDate: Sun Aug 16 19:09:09 2026 +0800
Commit:     Jun Zhao <[email protected]>
CommitDate: Mon Sep 14 12:44:07 2026 +0000

    lavf: add ASTC and KTX 1.0 muxers/demuxers
    
    Add single-image ASTC and KTX 1.0 muxers and demuxers for the
    libastcenc codec. Validate the KTX layout, ASTC dimensions, color profile,
    and compressed payload boundaries before exposing a packet to the decoder.
    
    Map the KTX 1.0 2D linear and sRGB ASTC formats to the corresponding
    libastcenc profiles, and reject unsupported HDR, 3D, array, cubemap, and
    mipmap layouts instead of producing ambiguous image streams.
    
    The KTX demuxer turns the KTXorientation key into display metadata, and the
    KTX muxer writes the key back, so a stream copied from a flipped texture
    keeps its row order. Its srgb option follows the encoder's color profile by
    default, and an explicit value that contradicts a known profile is rejected
    instead of silently relabelling the texture.
    
    Signed-off-by: Jun Zhao <[email protected]>
---
 libavformat/Makefile     |   4 +
 libavformat/allformats.c |   4 +
 libavformat/astcdec.c    | 175 ++++++++++++++++++++++++
 libavformat/astcenc.c    |  86 ++++++++++++
 libavformat/ktxdec.c     | 348 +++++++++++++++++++++++++++++++++++++++++++++++
 libavformat/ktxenc.c     | 295 +++++++++++++++++++++++++++++++++++++++
 libavformat/version.h    |   2 +-
 7 files changed, 913 insertions(+), 1 deletion(-)

diff --git a/libavformat/Makefile b/libavformat/Makefile
index 038e0afd41..3176be8a71 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -147,6 +147,8 @@ OBJS-$(CONFIG_ASS_DEMUXER)               += assdec.o 
subtitles.o
 OBJS-$(CONFIG_ASS_MUXER)                 += assenc.o
 OBJS-$(CONFIG_AST_DEMUXER)               += ast.o astdec.o
 OBJS-$(CONFIG_AST_MUXER)                 += ast.o astenc.o
+OBJS-$(CONFIG_ASTC_MUXER)                += astcenc.o
+OBJS-$(CONFIG_ASTC_DEMUXER)              += astcdec.o
 OBJS-$(CONFIG_AU_DEMUXER)                += au.o pcm.o
 OBJS-$(CONFIG_AU_MUXER)                  += au.o rawenc.o
 OBJS-$(CONFIG_AVI_DEMUXER)               += avidec.o
@@ -347,6 +349,8 @@ OBJS-$(CONFIG_JACOSUB_DEMUXER)           += jacosubdec.o 
subtitles.o
 OBJS-$(CONFIG_JACOSUB_MUXER)             += jacosubenc.o rawenc.o
 OBJS-$(CONFIG_JPEGXL_ANIM_DEMUXER)       += jpegxl_anim_dec.o
 OBJS-$(CONFIG_JV_DEMUXER)                += jvdec.o
+OBJS-$(CONFIG_KTX_MUXER)                 += ktxenc.o
+OBJS-$(CONFIG_KTX_DEMUXER)               += ktxdec.o
 OBJS-$(CONFIG_KUX_DEMUXER)               += flvdec.o
 OBJS-$(CONFIG_KVAG_DEMUXER)              += kvag.o
 OBJS-$(CONFIG_KVAG_MUXER)                += kvag.o rawenc.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index 162eaa42b9..17dcc53c29 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -88,6 +88,8 @@ extern const FFOutputFormat ff_ass_muxer;
 extern const FFInputFormat  ff_ast_demuxer;
 extern const FFOutputFormat ff_ast_muxer;
 extern const FFOutputFormat ff_asf_stream_muxer;
+extern const FFInputFormat  ff_astc_demuxer;
+extern const FFOutputFormat ff_astc_muxer;
 extern const FFInputFormat  ff_au_demuxer;
 extern const FFOutputFormat ff_au_muxer;
 extern const FFInputFormat  ff_av1_demuxer;
@@ -251,6 +253,8 @@ extern const FFInputFormat  ff_jacosub_demuxer;
 extern const FFOutputFormat ff_jacosub_muxer;
 extern const FFInputFormat  ff_jv_demuxer;
 extern const FFInputFormat  ff_jpegxl_anim_demuxer;
+extern const FFInputFormat  ff_ktx_demuxer;
+extern const FFOutputFormat ff_ktx_muxer;
 extern const FFInputFormat  ff_kux_demuxer;
 extern const FFInputFormat  ff_kvag_demuxer;
 extern const FFOutputFormat ff_kvag_muxer;
diff --git a/libavformat/astcdec.c b/libavformat/astcdec.c
new file mode 100644
index 0000000000..43699f7d30
--- /dev/null
+++ b/libavformat/astcdec.c
@@ -0,0 +1,175 @@
+/*
+ * ASTC demuxer (.astc container)
+ * Copyright (c) 2026 Jun Zhao
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * ASTC (Adaptive Scalable Texture Compression) demuxer.
+ *
+ * Reads the 16-byte .astc header, exposes it as extradata to the decoder,
+ * and emits the raw ASTC bitstream as a single packet.
+ */
+
+#include "config_components.h"
+
+#include <inttypes.h>
+#include <limits.h>
+
+#include "avformat.h"
+#include "avio.h"
+#include "avio_internal.h"
+#include "demux.h"
+#include "internal.h"
+#include "libavutil/intreadwrite.h"
+
+#define ASTC_HEADER_SIZE 16
+#define ASTC_BLOCK_BYTES 16
+static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C };
+
+typedef struct ASTCDemuxerContext {
+    uint64_t image_size;
+    int      truncated;
+} ASTCDemuxerContext;
+
+static int astc_probe(const AVProbeData *p)
+{
+    if (p->buf_size >= ASTC_HEADER_SIZE &&
+        !memcmp(p->buf, astc_magic, sizeof(astc_magic)))
+        return AVPROBE_SCORE_MAX;
+    return 0;
+}
+
+static int astc_read_header(AVFormatContext *s)
+{
+    AVIOContext *pb = s->pb;
+    ASTCDemuxerContext *astc = s->priv_data;
+    AVStream *st;
+    uint8_t hdr[ASTC_HEADER_SIZE];
+    unsigned int bx, by, bz, w, h, dz;
+    uint64_t expected;
+
+    if (ffio_read_size(pb, hdr, ASTC_HEADER_SIZE) < 0)
+        return AVERROR_INVALIDDATA;
+
+    if (memcmp(hdr, astc_magic, sizeof(astc_magic))) {
+        av_log(s, AV_LOG_ERROR, "Not an ASTC file (bad magic).\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    bx = hdr[4];
+    by = hdr[5];
+    bz = hdr[6];
+    w  = AV_RL24(hdr + 7);
+    h  = AV_RL24(hdr + 10);
+    dz = AV_RL24(hdr + 13);
+
+    /* Validate the geometry before trusting it to size the packet: the
+     * decoder only handles 2D images, and a zero block size would divide
+     * by zero when deriving the block count. */
+    if (!bx || !by || !bz || !w || !h || dz != 1) {
+        av_log(s, AV_LOG_ERROR,
+               "Invalid ASTC header: block %ux%ux%u, image %ux%ux%u.\n",
+               bx, by, bz, w, h, dz);
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* Multiply the block counts in 64 bits: the dimensions fit 24 bits, but
+     * their product overflows 32 bits for large images, and a wrapped result
+     * would under-size the packet, or wrap to zero and look like EOF. */
+    expected = (((uint64_t)w  + bx - 1) / bx) *
+               (((uint64_t)h  + by - 1) / by) *
+               (((uint64_t)dz + bz - 1) / bz) * ASTC_BLOCK_BYTES;
+    if (expected > INT_MAX) {
+        av_log(s, AV_LOG_ERROR, "ASTC image is too large (%"PRIu64" bytes).\n",
+               expected);
+        return AVERROR_INVALIDDATA;
+    }
+    astc->image_size = expected;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_ASTC;
+    st->codecpar->width      = w;
+    st->codecpar->height     = h;
+    st->codecpar->format     = AV_PIX_FMT_RGBA;
+
+    if (ff_alloc_extradata(st->codecpar, ASTC_HEADER_SIZE) < 0)
+        return AVERROR(ENOMEM);
+    memcpy(st->codecpar->extradata, hdr, ASTC_HEADER_SIZE);
+
+    /* Single image container. */
+    st->duration = 1;
+    avpriv_set_pts_info(st, 64, 1, 1);
+
+    return 0;
+}
+
+static int astc_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    ASTCDemuxerContext *astc = s->priv_data;
+    int ret;
+
+    /* Keep reporting a detected truncation instead of falling through to EOF:
+     * a caller that stops at the first error must not see the broken image as
+     * a clean end of stream. */
+    if (astc->truncated)
+        return AVERROR_INVALIDDATA;
+
+    if (!astc->image_size)
+        return AVERROR_EOF;
+
+    /* Read exactly one image worth of blocks; a short read means the input
+     * is truncated, and any trailing data is not part of this image. A payload
+     * that is missing entirely reads as EOF, which has to be reported as the
+     * truncation it is rather than as the end of a valid stream. */
+    ret = av_get_packet(s->pb, pkt, astc->image_size);
+    if (ret == AVERROR_EOF)
+        ret = 0;
+    if (ret < 0)
+        return ret;
+    if (ret != astc->image_size) {
+        av_log(s, AV_LOG_ERROR,
+               "Truncated ASTC image: got %d of %"PRIu64" bytes.\n",
+               ret, astc->image_size);
+        av_packet_unref(pkt);
+        astc->truncated = 1;
+        return AVERROR_INVALIDDATA;
+    }
+
+    astc->image_size = 0;
+    pkt->stream_index = 0;
+    pkt->flags |= AV_PKT_FLAG_KEY;
+    return 0;
+}
+
+const FFInputFormat ff_astc_demuxer = {
+    .p.name         = "astc",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("ASTC (Adaptive Scalable Texture 
Compression)"),
+    .p.mime_type    = "image/astc",
+    .p.extensions   = "astc",
+    .p.flags        = AVFMT_NOTIMESTAMPS,
+    .priv_data_size = sizeof(ASTCDemuxerContext),
+    .read_probe     = astc_probe,
+    .read_header    = astc_read_header,
+    .read_packet    = astc_read_packet,
+};
diff --git a/libavformat/astcenc.c b/libavformat/astcenc.c
new file mode 100644
index 0000000000..ef30e44158
--- /dev/null
+++ b/libavformat/astcenc.c
@@ -0,0 +1,86 @@
+/*
+ * ASTC muxer (.astc container)
+ * Copyright (c) 2026 Jun Zhao
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * ASTC (Adaptive Scalable Texture Compression) muxer.
+ *
+ * The .astc container is a 16-byte header followed by the raw ASTC
+ * bitstream. The header is taken verbatim from the encoder's extradata
+ * (which it publishes in .astc form).
+ */
+
+#include "avformat.h"
+#include "avio.h"
+#include "mux.h"
+
+#define ASTC_HEADER_SIZE 16
+
+typedef struct ASTCMuxerContext {
+    int wrote_image;
+} ASTCMuxerContext;
+
+static int astc_write_header(AVFormatContext *s)
+{
+    AVStream *st = s->streams[0];
+
+    if (st->codecpar->extradata_size < ASTC_HEADER_SIZE) {
+        av_log(s, AV_LOG_ERROR, ".astc muxer requires 16-byte extradata "
+               "(block size / dimensions) from the encoder.\n");
+        return AVERROR(EINVAL);
+    }
+
+    avio_write(s->pb, st->codecpar->extradata, ASTC_HEADER_SIZE);
+    return 0;
+}
+
+static int astc_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    ASTCMuxerContext *ctx = s->priv_data;
+
+    /* The container holds a single image; FF_OFMT_FLAG_MAX_ONE_OF_EACH only
+     * limits the number of streams, so packets must be rejected here. */
+    if (ctx->wrote_image) {
+        av_log(s, AV_LOG_ERROR,
+               ".astc muxer supports a single image per file.\n");
+        return AVERROR(EINVAL);
+    }
+    ctx->wrote_image = 1;
+
+    avio_write(s->pb, pkt->data, pkt->size);
+    return 0;
+}
+
+const FFOutputFormat ff_astc_muxer = {
+    .p.name         = "astc",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("ASTC (Adaptive Scalable Texture 
Compression)"),
+    .p.mime_type    = "image/astc",
+    .p.extensions   = "astc",
+    .p.audio_codec  = AV_CODEC_ID_NONE,
+    .p.video_codec  = AV_CODEC_ID_ASTC,
+    .p.subtitle_codec = AV_CODEC_ID_NONE,
+    .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH |
+                      FF_OFMT_FLAG_ONLY_DEFAULT_CODECS,
+    .priv_data_size = sizeof(ASTCMuxerContext),
+    .write_header   = astc_write_header,
+    .write_packet   = astc_write_packet,
+    .p.flags        = AVFMT_NOTIMESTAMPS,
+};
diff --git a/libavformat/ktxdec.c b/libavformat/ktxdec.c
new file mode 100644
index 0000000000..cebdb19420
--- /dev/null
+++ b/libavformat/ktxdec.c
@@ -0,0 +1,348 @@
+/*
+ * KTX 1.0 demuxer for ASTC
+ * Copyright (c) 2026 Jun Zhao
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * KTX 1.0 demuxer for ASTC textures.
+ *
+ * Parses the KTX 1.0 header, recovers the ASTC block size from the GL
+ * internal format enum, and synthesizes a 16-byte .astc-style extradata
+ * for the decoder. The raw ASTC bitstream is emitted as a single packet.
+ */
+
+#include "config_components.h"
+
+#include <inttypes.h>
+#include <limits.h>
+
+#include "libavcodec/defs.h"
+#include "avformat.h"
+#include "avio.h"
+#include "avio_internal.h"
+#include "demux.h"
+#include "internal.h"
+#include "libavutil/display.h"
+#include "libavutil/intreadwrite.h"
+
+#define ASTC_HEADER_SIZE 16
+static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C };
+static const uint8_t ktx_magic[12] = {
+    0xAB, 'K', 'T', 'X', ' ', '1', '1', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
+};
+
+/* ASTC GL internal format enums (2D), linear base 0x93B0, sRGB = +0x20. */
+static const int astc_gl_linear[14] = {
+    0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7,
+    0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD
+};
+static const int astc_bx[14] = { 4, 5, 5, 6, 6, 8, 8, 8, 10, 10, 10, 10, 12, 
12 };
+static const int astc_by[14] = { 4, 4, 5, 5, 6, 5, 6, 8, 5, 6, 8, 10, 10, 12 };
+
+typedef struct KTXDemuxerContext {
+    uint32_t image_size;
+    int      truncated;
+} KTXDemuxerContext;
+
+/* Map a GL internal format enum to a 2D ASTC block size. Returns 1 on 
success. */
+static int gl_enum_to_block(uint32_t e, int *bx, int *by)
+{
+    int base = (e >= 0x93D0 && e <= 0x93DD) ? (e - 0x20) : e;
+    for (int i = 0; i < 14; i++) {
+        if (astc_gl_linear[i] == (int)base) {
+            *bx = astc_bx[i];
+            *by = astc_by[i];
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int ktx_probe(const AVProbeData *p)
+{
+    int bx, by;
+
+    if (p->buf_size < 32 || memcmp(p->buf, ktx_magic, sizeof(ktx_magic)))
+        return 0;
+
+    /* KTX 1.0 carries ETC, uncompressed and other payloads this demuxer cannot
+     * read, so only claim the file once glInternalFormat is an ASTC format we
+     * actually support. */
+    if (!gl_enum_to_block(AV_RL32(p->buf + 28), &bx, &by))
+        return 0;
+
+    /* Just below the maximum: this is a KTX file, but only the ASTC subset of
+     * the container is implemented here. */
+    return AVPROBE_SCORE_MAX - 1;
+}
+
+/* Map a KTXorientation value such as "S=r,T=d" onto the flips needed to
+ * bring the stored rows back to the top-down, left-to-right order the
+ * decoder produces. The R component is irrelevant for 2D textures. */
+static void ktx_orientation_flips(const char *value, int *hflip, int *vflip)
+{
+    const char *p = value;
+
+    while (p && *p) {
+        if (p[0] == 'S' && p[1] == '=' && p[2])
+            *hflip = p[2] == 'l';
+        else if (p[0] == 'T' && p[1] == '=' && p[2])
+            *vflip = p[2] == 'u';
+        p = strchr(p, ',');
+        if (p)
+            p++;
+    }
+}
+
+/* Walk the key/value block, picking up KTXorientation. Unknown or oversized
+ * entries are skipped rather than buffered wholesale. */
+static int ktx_read_orientation(AVFormatContext *s, uint32_t kvdata,
+                                int *hflip, int *vflip)
+{
+    AVIOContext *pb = s->pb;
+    uint8_t buf[256];
+    uint32_t left = kvdata;
+
+    *hflip = *vflip = 0;
+
+    while (left >= 4) {
+        uint32_t entry = avio_rl32(pb);
+        uint32_t padded = (entry + 3) & ~3u;
+
+        left -= 4;
+        if (!entry || entry > left || padded > left) {
+            av_log(s, AV_LOG_ERROR, "Invalid KTX key/value data.\n");
+            return AVERROR_INVALIDDATA;
+        }
+
+        if (entry >= sizeof(buf)) {
+            /* Nothing interesting is this large. */
+            if (avio_skip(pb, padded) < 0)
+                return AVERROR_INVALIDDATA;
+        } else {
+            const char *key, *value;
+
+            if (ffio_read_size(pb, buf, entry) < 0)
+                return AVERROR_INVALIDDATA;
+            buf[entry] = 0;
+
+            key = (const char *)buf;
+            value = key + strlen(key) + 1;
+            if (value < (const char *)buf + entry) {
+                if (!strcmp(key, "KTXorientation"))
+                    ktx_orientation_flips(value, hflip, vflip);
+                else
+                    av_log(s, AV_LOG_VERBOSE, "Ignoring KTX key '%s'.\n", key);
+            }
+            if (avio_skip(pb, padded - entry) < 0)
+                return AVERROR_INVALIDDATA;
+        }
+        left -= padded;
+    }
+
+    /* Every entry occupies a padded, four byte aligned slot, so a leftover
+     * tail means the declared block size is not aligned and the image payload
+     * does not start where the header claims it does. */
+    if (left) {
+        av_log(s, AV_LOG_ERROR, "Invalid KTX key/value data.\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    return 0;
+}
+
+static int ktx_read_header(AVFormatContext *s)
+{
+    AVIOContext *pb = s->pb;
+    KTXDemuxerContext *ktx = s->priv_data;
+    AVStream *st;
+    uint8_t hdr[64];
+    uint32_t endian, gl_type, gl_type_size, gl_format, gl_internal;
+    uint32_t gl_base_internal, w, h, depth, array, faces, mips;
+    uint32_t kvdata, image_size;
+    uint8_t image_size_buf[4];
+    int bx = 0, by = 0;
+    int hflip = 0, vflip = 0;
+    uint8_t extra[16];
+
+    if (ffio_read_size(pb, hdr, sizeof(hdr)) < 0)
+        return AVERROR_INVALIDDATA;
+    if (memcmp(hdr, ktx_magic, sizeof(ktx_magic))) {
+        av_log(s, AV_LOG_ERROR, "Not a KTX file (bad magic).\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    endian          = AV_RL32(hdr + 12);
+    gl_type         = AV_RL32(hdr + 16);
+    gl_type_size    = AV_RL32(hdr + 20);
+    gl_format       = AV_RL32(hdr + 24);
+    gl_internal     = AV_RL32(hdr + 28);
+    gl_base_internal = AV_RL32(hdr + 32);
+    w               = AV_RL32(hdr + 36);
+    h               = AV_RL32(hdr + 40);
+    depth           = AV_RL32(hdr + 44);
+    array           = AV_RL32(hdr + 48);
+    faces           = AV_RL32(hdr + 52);
+    mips            = AV_RL32(hdr + 56);
+    kvdata          = AV_RL32(hdr + 60);
+
+    if (endian != 0x04030201) {
+        av_log(s, AV_LOG_ERROR, "KTX big-endian / unknown endianness not 
supported.\n");
+        return AVERROR_INVALIDDATA;
+    }
+    if (gl_type || gl_type_size != 1 || gl_format || gl_base_internal != 
0x1908 ||
+        !w || !h || w > 0xFFFFFF || h > 0xFFFFFF ||
+        depth || array || faces != 1 || mips != 1) {
+        av_log(s, AV_LOG_ERROR, "Unsupported KTX texture layout.\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (!gl_enum_to_block(gl_internal, &bx, &by)) {
+        av_log(s, AV_LOG_ERROR, "Unsupported ASTC GL format 0x%X in KTX.\n", 
gl_internal);
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (ktx_read_orientation(s, kvdata, &hflip, &vflip) < 0)
+        return AVERROR_INVALIDDATA;
+    if (ffio_read_size(pb, image_size_buf, sizeof(image_size_buf)) < 0)
+        return AVERROR_INVALIDDATA;
+    image_size = AV_RL32(image_size_buf);
+    if (!image_size || image_size > INT_MAX)
+        return AVERROR_INVALIDDATA;
+    /* The payload must hold exactly the blocks the texture geometry needs;
+     * anything else would either truncate the image or smuggle in trailing
+     * data that the decoder would ignore. */
+    {
+        uint64_t expected = (((uint64_t)w + bx - 1) / bx) *
+                            (((uint64_t)h + by - 1) / by) * 16;
+        if (image_size != expected) {
+            av_log(s, AV_LOG_ERROR,
+                   "KTX image size %u does not match the %"PRIu64" bytes "
+                   "required for a %ux%u texture with %dx%d blocks.\n",
+                   image_size, expected, w, h, bx, by);
+            return AVERROR_INVALIDDATA;
+        }
+    }
+    ktx->image_size = image_size;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_ASTC;
+    st->codecpar->width      = w;
+    st->codecpar->height     = h;
+    /* The GL internal format fixes the colour space but not the endpoint
+     * format: an sRGB texture is LDR, while a linear one may hold LDR, HDR RGB
+     * with LDR alpha, or full HDR endpoints. Report that distinction so the
+     * decoder does not assume LDR for a linear texture; a linear texture is
+     * sampled with HDR precision by default (LINEAR_ANY), so the stream format
+     * is the matching half-float one rather than 8-bit RGBA. */
+    st->codecpar->profile    = gl_internal >= 0x93D0 && gl_internal <= 0x93DD ?
+                               AV_PROFILE_ASTC_LDR_SRGB : 
AV_PROFILE_ASTC_LINEAR_ANY;
+    st->codecpar->format     = st->codecpar->profile == 
AV_PROFILE_ASTC_LDR_SRGB ?
+                               AV_PIX_FMT_RGBA : AV_PIX_FMT_RGBAF16;
+
+    /* Synthesize the .astc-style extradata for the decoder. Bytes [13-15] hold
+     * the image depth (dim_z), which is always 1 here: KTX 1.0 2D only. */
+    memset(extra, 0, sizeof(extra));
+    memcpy(extra, astc_magic, sizeof(astc_magic));
+    extra[4] = (uint8_t)bx;
+    extra[5] = (uint8_t)by;
+    extra[6] = 1;
+    AV_WL24(extra + 7,  w);
+    AV_WL24(extra + 10, h);
+    extra[13] = 1;
+    if (ff_alloc_extradata(st->codecpar, ASTC_HEADER_SIZE) < 0)
+        return AVERROR(ENOMEM);
+    memcpy(st->codecpar->extradata, extra, ASTC_HEADER_SIZE);
+
+    /* Carry the stored row order to the caller instead of silently returning
+     * a mirrored image. */
+    if (hflip || vflip) {
+        AVPacketSideData *sd;
+        int32_t *matrix;
+
+        sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
+                                     &st->codecpar->nb_coded_side_data,
+                                     AV_PKT_DATA_DISPLAYMATRIX,
+                                     9 * sizeof(*matrix), 0);
+        if (!sd)
+            return AVERROR(ENOMEM);
+        matrix = (int32_t *)sd->data;
+        av_display_rotation_set(matrix, 0);
+        av_display_matrix_flip(matrix, hflip, vflip);
+        av_log(s, AV_LOG_VERBOSE,
+               "Applying KTXorientation (hflip %d, vflip %d).\n", hflip, 
vflip);
+    }
+
+    avpriv_set_pts_info(st, 64, 1, 1);
+    return 0;
+}
+
+static int ktx_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    KTXDemuxerContext *ktx = s->priv_data;
+    int ret;
+
+    /* Keep reporting a detected truncation instead of falling through to EOF:
+     * a caller that stops at the first error must not see the broken image as
+     * a clean end of stream. */
+    if (ktx->truncated)
+        return AVERROR_INVALIDDATA;
+
+    if (!ktx->image_size)
+        return AVERROR_EOF;
+
+    /* A payload that is missing entirely reads as EOF, which has to be
+     * reported as the truncation it is rather than as the end of a valid
+     * stream. */
+    ret = av_get_packet(s->pb, pkt, ktx->image_size);
+    if (ret == AVERROR_EOF)
+        ret = 0;
+    if (ret < 0)
+        return ret;
+    if (ret != ktx->image_size) {
+        av_log(s, AV_LOG_ERROR,
+               "Truncated KTX image: got %d of %u bytes.\n",
+               ret, ktx->image_size);
+        av_packet_unref(pkt);
+        ktx->truncated = 1;
+        return AVERROR_INVALIDDATA;
+    }
+
+    pkt->stream_index = 0;
+    pkt->flags |= AV_PKT_FLAG_KEY;
+    ktx->image_size = 0; /* single mip */
+    return 0;
+}
+
+const FFInputFormat ff_ktx_demuxer = {
+    .p.name         = "ktx",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("KTX 1.0 (Khronos Texture) for 
ASTC"),
+    .p.mime_type    = "image/ktx",
+    .p.extensions   = "ktx",
+    .p.flags        = AVFMT_NOTIMESTAMPS,
+    .priv_data_size = sizeof(KTXDemuxerContext),
+    .read_probe     = ktx_probe,
+    .read_header    = ktx_read_header,
+    .read_packet    = ktx_read_packet,
+};
diff --git a/libavformat/ktxenc.c b/libavformat/ktxenc.c
new file mode 100644
index 0000000000..f1f374aa13
--- /dev/null
+++ b/libavformat/ktxenc.c
@@ -0,0 +1,295 @@
+/*
+ * KTX 1.0 muxer for ASTC
+ * Copyright (c) 2026 Jun Zhao
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * KTX 1.0 muxer for ASTC textures.
+ *
+ * Writes the KTX 1.0 header (64 bytes) followed by a uint32 image size and
+ * the raw ASTC bitstream. The block size is recovered from the encoder's
+ * .astc-style extradata; the sRGB vs linear GL enum is chosen by the
+ * "srgb" muxer option.
+ */
+
+#include "avformat.h"
+#include "avio.h"
+#include "mux.h"
+#include "libavcodec/defs.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/opt.h"
+
+#define ASTC_HEADER_SIZE 16
+static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C };
+
+/* ASTC GL internal format enums (2D), linear base 0x93B0, sRGB = +0x20.
+ * Order matches the astc-encoder ASTC_FORMATS table. */
+static const int astc_gl_linear[14] = {
+    0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7,
+    0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD
+};
+static const int astc_gl_srgb[14] = {
+    0x93D0, 0x93D1, 0x93D2, 0x93D3, 0x93D4, 0x93D5, 0x93D6, 0x93D7,
+    0x93D8, 0x93D9, 0x93DA, 0x93DB, 0x93DC, 0x93DD
+};
+/* Block sizes paired with the enum tables above. */
+static const int astc_bx[14] = { 4, 5, 5, 6, 6, 8, 8, 8, 10, 10, 10, 10, 12, 
12 };
+static const int astc_by[14] = { 4, 4, 5, 5, 6, 5, 6, 8, 5, 6, 8, 10, 10, 12 };
+
+typedef struct KTXMuxerContext {
+    AVClass *class;
+    int srgb;        /* -1 = auto (follow the encoder profile) */
+    int wrote_image;
+} KTXMuxerContext;
+
+static const char ktx_orientation_key[] = "KTXorientation";
+
+/* Derive the stored row order from the stream's display matrix. The demuxer
+ * converts KTXorientation into exactly such a matrix, so a stream copied from
+ * a flipped KTX keeps its orientation instead of being relabelled. Any other
+ * transform is rejected rather than silently dropped: KTXorientation only has
+ * room for axis flips, and applying a rotation or a scale to the pixels would
+ * mean decoding and re-encoding the texture. */
+static int ktx_orientation_from_side_data(AVFormatContext *s, AVStream *st,
+                                          int *hflip, int *vflip)
+{
+    const AVPacketSideData *sd;
+    const int32_t *matrix;
+
+    *hflip = *vflip = 0;
+    sd = av_packet_side_data_get(st->codecpar->coded_side_data,
+                                 st->codecpar->nb_coded_side_data,
+                                 AV_PKT_DATA_DISPLAYMATRIX);
+    if (!sd || sd->size < 9 * sizeof(*matrix))
+        return 0;
+
+    matrix = (const int32_t *)sd->data;
+    /* The layout av_display_matrix_flip() produces: the 2x2 part (a, b, c, d)
+     * is diagonal with unit scales, negative when the axis is flipped, the
+     * translation (x, y) is zero, and the perspective terms (u, v, w) are the
+     * identity ones. Note that a, b, c, d, x and y are 16.16 fixed-point while
+     * u, v and w are 2.30. */
+    if (matrix[1] || matrix[2] || matrix[3] || matrix[5] ||
+        matrix[6] || matrix[7] || matrix[8] != 1 << 30 ||
+        FFABSU(matrix[0]) != 65536 || FFABSU(matrix[4]) != 65536) {
+        av_log(s, AV_LOG_ERROR, "Unsupported display matrix: KTXorientation "
+               "only records axis flips, not rotations, scales, translations "
+               "or perspective.\n");
+        return AVERROR(EINVAL);
+    }
+
+    *hflip = matrix[0] < 0;
+    *vflip = matrix[4] < 0;
+    return 0;
+}
+
+static int ktx_write_header(AVFormatContext *s)
+{
+    KTXMuxerContext *ctx = s->priv_data;
+    AVStream *st = s->streams[0];
+    const uint8_t *ed = st->codecpar->extradata;
+    uint8_t bx = 0, by = 0;
+    int glfmt = 0, i, srgb, profile_srgb, ret;
+    int hflip, vflip, kv_size, kv_padded;
+    uint8_t hdr[64];
+    char kv[32];
+    char orientation[8];
+
+    if (st->codecpar->extradata_size < ASTC_HEADER_SIZE) {
+        av_log(s, AV_LOG_ERROR, ".ktx muxer requires 16-byte extradata "
+               "(block size) from the encoder.\n");
+        return AVERROR(EINVAL);
+    }
+    if (st->codecpar->width <= 0 || st->codecpar->height <= 0 ||
+        st->codecpar->width > 0xFFFFFF || st->codecpar->height > 0xFFFFFF) {
+        av_log(s, AV_LOG_ERROR, "Invalid image dimensions %dx%d.\n",
+               st->codecpar->width, st->codecpar->height);
+        return AVERROR(EINVAL);
+    }
+    if (memcmp(ed, astc_magic, sizeof(astc_magic)) ||
+        AV_RL24(ed + 7) != st->codecpar->width ||
+        AV_RL24(ed + 10) != st->codecpar->height ||
+        AV_RL24(ed + 13) != 1) {
+        av_log(s, AV_LOG_ERROR, ".ktx muxer received invalid ASTC 
extradata.\n");
+        return AVERROR_INVALIDDATA;
+    }
+    bx = ed[4];
+    by = ed[5];
+
+    /* ASTC bitstreams do not record whether they hold LDR or HDR endpoints,
+     * and the KTX 1.0 GL enums do not express it either: the linear ASTC
+     * formats can also carry HDR endpoint encodings. Writing HDR through this
+     * muxer is simply not implemented, so reject the profiles that name an HDR
+     * endpoint format and point at the .astc container. A stream that only
+     * reports a linear colour space is not known to be HDR and is still
+     * written as a linear texture. */
+    if (st->codecpar->profile == AV_PROFILE_ASTC_HDR_RGB_LDR_A ||
+        st->codecpar->profile == AV_PROFILE_ASTC_HDR) {
+        av_log(s, AV_LOG_ERROR,
+               "Writing HDR ASTC to KTX 1.0 is not supported; use the .astc "
+               "container for HDR output.\n");
+        return AVERROR(EINVAL);
+    }
+    /* codecpar->profile publishes the colour space (and, when it is known, the
+     * endpoint format) of the stream. Only the profiles that name a colour
+     * space determine the GL enum here; a stream without profile information
+     * leaves it undetermined. */
+    switch (st->codecpar->profile) {
+    case AV_PROFILE_ASTC_LDR_SRGB:
+        profile_srgb = 1;
+        break;
+    case AV_PROFILE_ASTC_LDR:
+    case AV_PROFILE_ASTC_LINEAR_ANY:
+        profile_srgb = 0;
+        break;
+    default:
+        profile_srgb = -1;
+        break;
+    }
+
+    /* Auto keeps the color space the encoder was configured with, so that
+     * remuxing a linear KTX does not silently flip it to sRGB; a stream
+     * without profile information falls back to the sRGB ASTC format. */
+    srgb = ctx->srgb >= 0 ? ctx->srgb :
+           profile_srgb >= 0 ? profile_srgb : 1;
+
+    /* An explicit request must not contradict a known encoder profile. */
+    if (ctx->srgb >= 0 && profile_srgb >= 0 && profile_srgb != ctx->srgb) {
+        av_log(s, AV_LOG_ERROR,
+               "KTX srgb option does not match the encoder profile.\n");
+        return AVERROR(EINVAL);
+    }
+
+    /* Writing 3D ASTC blocks is not implemented: only the 2D GL internal
+     * formats are emitted. */
+    if (ed[6] != 1) {
+        av_log(s, AV_LOG_ERROR,
+               "Writing 3D ASTC blocks to KTX is not supported; use the .astc "
+               "container for 3D block output.\n");
+        return AVERROR(EINVAL);
+    }
+
+    for (i = 0; i < 14; i++) {
+        if (astc_bx[i] == bx && astc_by[i] == by) {
+            glfmt = srgb ? astc_gl_srgb[i] : astc_gl_linear[i];
+            break;
+        }
+    }
+    if (!glfmt) {
+        av_log(s, AV_LOG_ERROR, "Unsupported ASTC block size %dx%d for 
KTX.\n", bx, by);
+        return AVERROR(EINVAL);
+    }
+
+    /* The stored rows are passed through unchanged, so describe them as they
+     * are: the encoder writes them top-down, left-to-right, unless the input
+     * brings its own orientation from a previous KTX. */
+    ret = ktx_orientation_from_side_data(s, st, &hflip, &vflip);
+    if (ret < 0)
+        return ret;
+    memcpy(orientation, "S=r,T=d", sizeof(orientation));
+    orientation[2] = hflip ? 'l' : 'r';
+    orientation[6] = vflip ? 'u' : 'd';
+
+    memcpy(kv, ktx_orientation_key, sizeof(ktx_orientation_key));
+    memcpy(kv + sizeof(ktx_orientation_key), orientation, sizeof(orientation));
+    kv_size   = sizeof(ktx_orientation_key) + sizeof(orientation);
+    kv_padded = (kv_size + 3) & ~3;
+
+    memset(hdr, 0, sizeof(hdr));
+    /* KTX 1.0 file identifier: 0xAB 'K' 'T' 'X' ' ' '1' '1' 0xBB 0x0D 0x0A 
0x1A 0x0A */
+    hdr[0] = 0xAB; hdr[1] = 'K'; hdr[2] = 'T'; hdr[3] = 'X';
+    hdr[4] = ' ';  hdr[5] = '1'; hdr[6] = '1'; hdr[7] = 0xBB;
+    hdr[8] = 0x0D; hdr[9] = 0x0A; hdr[10] = 0x1A; hdr[11] = 0x0A;
+    AV_WL32(hdr + 12, 0x04030201);  /* endianness */
+    /* gl_type(16)=0, gl_type_size(20)=1, gl_format(24)=0 */
+    AV_WL32(hdr + 20, 1);
+    AV_WL32(hdr + 28, glfmt);       /* gl_internal_format */
+    AV_WL32(hdr + 32, 0x1908);      /* gl_base_internal_format = GL_RGBA */
+    AV_WL32(hdr + 36, st->codecpar->width);   /* pixel_width */
+    AV_WL32(hdr + 40, st->codecpar->height);  /* pixel_height */
+    /* pixel_depth(44)=0, array(48)=0 */
+    AV_WL32(hdr + 52, 1); /* number_of_faces */
+    AV_WL32(hdr + 56, 1); /* number_of_mipmap_levels */
+    AV_WL32(hdr + 60, 4 + kv_padded); /* bytes_of_key_value_data */
+
+    avio_write(s->pb, hdr, sizeof(hdr));
+
+    /* Self-describe the row convention so readers do not have to guess
+     * whether the texture is stored top-down or bottom-up. */
+    avio_wl32(s->pb, kv_size);
+    avio_write(s->pb, kv, kv_size);
+    for (i = kv_size; i % 4; i++)
+        avio_w8(s->pb, 0);
+
+    return 0;
+}
+
+static int ktx_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    KTXMuxerContext *ctx = s->priv_data;
+
+    /* The container holds a single image; FF_OFMT_FLAG_MAX_ONE_OF_EACH only
+     * limits the number of streams, so packets must be rejected here. */
+    if (ctx->wrote_image) {
+        av_log(s, AV_LOG_ERROR,
+               ".ktx muxer supports a single image per file.\n");
+        return AVERROR(EINVAL);
+    }
+    ctx->wrote_image = 1;
+
+    avio_wl32(s->pb, pkt->size);     /* imageSize */
+    avio_write(s->pb, pkt->data, pkt->size);
+    return 0;
+}
+
+#define OFFSET(x) offsetof(KTXMuxerContext, x)
+#define VE AV_OPT_FLAG_ENCODING_PARAM
+static const AVOption ktx_options[] = {
+    { "srgb", "GL internal format color space", OFFSET(srgb),
+      AV_OPT_TYPE_INT, { .i64 = -1 }, -1, 1, VE, .unit = "srgb" },
+    { "auto",   "Follow the encoder profile",   0, AV_OPT_TYPE_CONST, { .i64 = 
-1 }, 0, 0, VE, .unit = "srgb" },
+    { "linear", "Write the linear ASTC format", 0, AV_OPT_TYPE_CONST, { .i64 = 
 0 }, 0, 0, VE, .unit = "srgb" },
+    { "srgb",   "Write the sRGB ASTC format",   0, AV_OPT_TYPE_CONST, { .i64 = 
 1 }, 0, 0, VE, .unit = "srgb" },
+    { NULL },
+};
+
+static const AVClass ktx_muxer_class = {
+    .class_name = "ktx_muxer",
+    .item_name  = av_default_item_name,
+    .option     = ktx_options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+const FFOutputFormat ff_ktx_muxer = {
+    .p.name         = "ktx",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("KTX 1.0 (Khronos Texture) for 
ASTC"),
+    .p.mime_type    = "image/ktx",
+    .p.extensions   = "ktx",
+    .p.audio_codec  = AV_CODEC_ID_NONE,
+    .p.video_codec  = AV_CODEC_ID_ASTC,
+    .p.subtitle_codec = AV_CODEC_ID_NONE,
+    .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH |
+                      FF_OFMT_FLAG_ONLY_DEFAULT_CODECS,
+    .priv_data_size = sizeof(KTXMuxerContext),
+    .p.priv_class   = &ktx_muxer_class,
+    .write_header   = ktx_write_header,
+    .write_packet   = ktx_write_packet,
+    .p.flags        = AVFMT_NOTIMESTAMPS,
+};
diff --git a/libavformat/version.h b/libavformat/version.h
index 4bde82abb4..70c554c19c 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,7 +31,7 @@
 
 #include "version_major.h"
 
-#define LIBAVFORMAT_VERSION_MINOR   6
+#define LIBAVFORMAT_VERSION_MINOR   7
 #define LIBAVFORMAT_VERSION_MICRO 100
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \

-- 
To stop receiving notification emails like this one, please contact
[email protected].
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to