1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qktxhandler_p.h"
5#include "qtexturefiledata_p.h"
6#include <QtEndian>
7#include <QSize>
8#include <QMap>
9#include <QtCore/qiodevice.h>
10
11//#define KTX_DEBUG
12#ifdef KTX_DEBUG
13#include <QDebug>
14#include <QMetaEnum>
15#include <QOpenGLTexture>
16#endif
17
18QT_BEGIN_NAMESPACE
19
20using namespace Qt::StringLiterals;
21
22#define KTX_IDENTIFIER_LENGTH 12
23static const char ktxIdentifier[KTX_IDENTIFIER_LENGTH] = { '\xAB', 'K', 'T', 'X', ' ', '1', '1', '\xBB', '\r', '\n', '\x1A', '\n' };
24static const quint32 platformEndianIdentifier = 0x04030201;
25static const quint32 inversePlatformEndianIdentifier = 0x01020304;
26
27struct KTXHeader {
28 quint8 identifier[KTX_IDENTIFIER_LENGTH]; // Must match ktxIdentifier
29 quint32 endianness; // Either platformEndianIdentifier or inversePlatformEndianIdentifier, other values not allowed.
30 quint32 glType;
31 quint32 glTypeSize;
32 quint32 glFormat;
33 quint32 glInternalFormat;
34 quint32 glBaseInternalFormat;
35 quint32 pixelWidth;
36 quint32 pixelHeight;
37 quint32 pixelDepth;
38 quint32 numberOfArrayElements;
39 quint32 numberOfFaces;
40 quint32 numberOfMipmapLevels;
41 quint32 bytesOfKeyValueData;
42};
43
44static const quint32 qktxh_headerSize = sizeof(KTXHeader);
45
46// Currently unused, declared for future reference
47struct KTXKeyValuePairItem {
48 quint32 keyAndValueByteSize;
49 /*
50 quint8 keyAndValue[keyAndValueByteSize];
51 quint8 valuePadding[3 - ((keyAndValueByteSize + 3) % 4)];
52 */
53};
54
55struct KTXMipmapLevel {
56 quint32 imageSize;
57 /*
58 for each array_element in numberOfArrayElements*
59 for each face in numberOfFaces
60 for each z_slice in pixelDepth*
61 for each row or row_of_blocks in pixelHeight*
62 for each pixel or block_of_pixels in pixelWidth
63 Byte data[format-specific-number-of-bytes]**
64 end
65 end
66 end
67 Byte cubePadding[0-3]
68 end
69 end
70 quint8 mipPadding[3 - ((imageSize + 3) % 4)]
71 */
72};
73
74// Returns the nearest multiple of 'rounding' greater than or equal to 'value'
75constexpr quint32 withPadding(quint32 value, quint32 rounding)
76{
77 Q_ASSERT(rounding > 1);
78 return value + (rounding - 1) - ((value + (rounding - 1)) % rounding);
79}
80
81QKtxHandler::~QKtxHandler() = default;
82
83bool QKtxHandler::canRead(const QByteArray &suffix, const QByteArray &block)
84{
85 Q_UNUSED(suffix);
86
87 return (qstrncmp(str1: block.constData(), str2: ktxIdentifier, KTX_IDENTIFIER_LENGTH) == 0);
88}
89
90QTextureFileData QKtxHandler::read()
91{
92 if (!device())
93 return QTextureFileData();
94
95 const QByteArray buf = device()->readAll();
96 const quint32 dataSize = quint32(buf.size());
97 if (dataSize < qktxh_headerSize || !canRead(suffix: QByteArray(), block: buf)) {
98 qCDebug(lcQtGuiTextureIO, "Invalid KTX file %s", logName().constData());
99 return QTextureFileData();
100 }
101
102 const KTXHeader *header = reinterpret_cast<const KTXHeader *>(buf.data());
103 if (!checkHeader(header: *header)) {
104 qCDebug(lcQtGuiTextureIO, "Unsupported KTX file format in %s", logName().constData());
105 return QTextureFileData();
106 }
107
108 QTextureFileData texData;
109 texData.setData(buf);
110
111 texData.setSize(QSize(decode(val: header->pixelWidth), decode(val: header->pixelHeight)));
112 texData.setGLFormat(decode(val: header->glFormat));
113 texData.setGLInternalFormat(decode(val: header->glInternalFormat));
114 texData.setGLBaseInternalFormat(decode(val: header->glBaseInternalFormat));
115
116 texData.setNumLevels(decode(val: header->numberOfMipmapLevels));
117 texData.setNumFaces(decode(val: header->numberOfFaces));
118
119 const quint32 bytesOfKeyValueData = decode(val: header->bytesOfKeyValueData);
120 if (qktxh_headerSize + bytesOfKeyValueData < quint64(buf.size())) // oob check
121 texData.setKeyValueMetadata(decodeKeyValues(
122 view: QByteArrayView(buf.data() + qktxh_headerSize, bytesOfKeyValueData)));
123 quint32 offset = qktxh_headerSize + bytesOfKeyValueData;
124
125 constexpr int MAX_ITERATIONS = 32; // cap iterations in case of corrupt data
126
127 for (int level = 0; level < qMin(a: texData.numLevels(), b: MAX_ITERATIONS); level++) {
128 if (offset + sizeof(quint32) > dataSize) // Corrupt file; avoid oob read
129 break;
130
131 const quint32 imageSize = decode(val: qFromUnaligned<quint32>(src: buf.data() + offset));
132 offset += sizeof(quint32);
133
134 for (int face = 0; face < qMin(a: texData.numFaces(), b: MAX_ITERATIONS); face++) {
135 texData.setDataOffset(offset, level, face);
136 texData.setDataLength(length: imageSize, level, face);
137
138 // Add image data and padding to offset
139 offset += withPadding(value: imageSize, rounding: 4);
140 }
141 }
142
143 if (!texData.isValid()) {
144 qCDebug(lcQtGuiTextureIO, "Invalid values in header of KTX file %s", logName().constData());
145 return QTextureFileData();
146 }
147
148 texData.setLogName(logName());
149
150#ifdef KTX_DEBUG
151 qDebug() << "KTX file handler read" << texData;
152#endif
153
154 return texData;
155}
156
157bool QKtxHandler::checkHeader(const KTXHeader &header)
158{
159 if (header.endianness != platformEndianIdentifier && header.endianness != inversePlatformEndianIdentifier)
160 return false;
161 inverseEndian = (header.endianness == inversePlatformEndianIdentifier);
162#ifdef KTX_DEBUG
163 QMetaEnum tfme = QMetaEnum::fromType<QOpenGLTexture::TextureFormat>();
164 QMetaEnum ptme = QMetaEnum::fromType<QOpenGLTexture::PixelType>();
165 qDebug("Header of %s:", logName().constData());
166 qDebug(" glType: 0x%x (%s)", decode(header.glType), ptme.valueToKey(decode(header.glType)));
167 qDebug(" glTypeSize: %u", decode(header.glTypeSize));
168 qDebug(" glFormat: 0x%x (%s)", decode(header.glFormat),
169 tfme.valueToKey(decode(header.glFormat)));
170 qDebug(" glInternalFormat: 0x%x (%s)", decode(header.glInternalFormat),
171 tfme.valueToKey(decode(header.glInternalFormat)));
172 qDebug(" glBaseInternalFormat: 0x%x (%s)", decode(header.glBaseInternalFormat),
173 tfme.valueToKey(decode(header.glBaseInternalFormat)));
174 qDebug(" pixelWidth: %u", decode(header.pixelWidth));
175 qDebug(" pixelHeight: %u", decode(header.pixelHeight));
176 qDebug(" pixelDepth: %u", decode(header.pixelDepth));
177 qDebug(" numberOfArrayElements: %u", decode(header.numberOfArrayElements));
178 qDebug(" numberOfFaces: %u", decode(header.numberOfFaces));
179 qDebug(" numberOfMipmapLevels: %u", decode(header.numberOfMipmapLevels));
180 qDebug(" bytesOfKeyValueData: %u", decode(header.bytesOfKeyValueData));
181#endif
182 const bool isCompressedImage = decode(val: header.glType) == 0 && decode(val: header.glFormat) == 0
183 && decode(val: header.pixelDepth) == 0;
184 const bool isCubeMap = decode(val: header.numberOfFaces) == 6;
185 const bool is2D = decode(val: header.pixelDepth) == 0 && decode(val: header.numberOfArrayElements) == 0;
186
187 return is2D && (isCubeMap || isCompressedImage);
188}
189
190QMap<QByteArray, QByteArray> QKtxHandler::decodeKeyValues(QByteArrayView view) const
191{
192 QMap<QByteArray, QByteArray> output;
193 quint32 offset = 0;
194 while (offset < view.size() + sizeof(quint32)) {
195 const quint32 keyAndValueByteSize =
196 decode(val: qFromUnaligned<quint32>(src: view.constData() + offset));
197 offset += sizeof(quint32);
198
199 if (offset + keyAndValueByteSize > quint64(view.size()))
200 break; // oob read
201
202 // 'key' is a UTF-8 string ending with a null terminator, 'value' is the rest.
203 // To separate the key and value we convert the complete data to utf-8 and find the first
204 // null terminator from the left, here we split the data into two.
205 const auto str = QString::fromUtf8(utf8: view.constData() + offset, size: keyAndValueByteSize);
206 const int idx = str.indexOf(c: '\0'_L1);
207 if (idx == -1)
208 continue;
209
210 const QByteArray key = str.left(n: idx).toUtf8();
211 const size_t keySize = key.size() + 1; // Actual data size
212 const QByteArray value = QByteArray::fromRawData(data: view.constData() + offset + keySize,
213 size: keyAndValueByteSize - keySize);
214
215 offset = withPadding(value: offset + keyAndValueByteSize, rounding: 4);
216 output.insert(key, value);
217 }
218
219 return output;
220}
221
222quint32 QKtxHandler::decode(quint32 val) const
223{
224 return inverseEndian ? qbswap<quint32>(source: val) : val;
225}
226
227QT_END_NAMESPACE
228

source code of qtbase/src/gui/util/qktxhandler.cpp