Files
how-to-vulkan/ktx/lib/writer2.c
T
2026-06-14 19:09:18 +01:00

991 lines
34 KiB
C

/* -*- tab-width: 4; -*- */
/* vi: set sw=2 ts=4 expandtab: */
/**
* @internal
* @file
* @~English
*
* @brief Functions for creating KTX-format files from a set of images.
*
* @author Mark Callow, HI Corporation
*/
/*
* Copyright 2018-2020 Mark Callow.
* SPDX-License-Identifier: Apache-2.0
*/
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__GNUC__)
#include <strings.h> // For strncasecmp on GNU/Linux
#endif
#include <zstd.h>
#include <zstd_errors.h>
#include <KHR/khr_df.h>
#include "ktx.h"
#include "ktxint.h"
#include "filestream.h"
#include "memstream.h"
#include "texture2.h"
#include "dfdutils/dfd.h"
#include "vkformat_enum.h"
#include "vk_format.h"
#include "version.h"
#if defined(_MSC_VER)
#define strncasecmp _strnicmp
#endif
/**
* @defgroup writer Writer
* @brief Write KTX-formatted data.
* @{
*/
#if defined(_WIN32) || defined(linux) || defined(__linux) || defined(__linux__) || defined(__EMSCRIPTEN__)
/** @internal
* @~English
* @brief strnstr for Windows, Linux and Emscripten.
*
* strnstr is available in <apple>OS and BSD distributions. To use in Linux
* requires linking an additional library, libbsd. It is simpler to use ours.
*
* @param[in] haystack pointer to string to search.
* @param[in] needle pointer to string to search for.
* @param[in] len length of @p haystack string. Also used as limit to
* length of @p needle string.
*
* @return @p haystack, if @p needle is an empty string otherwise NULL, if
* @p needle does not occur in @p haystack, or a pointer to the
* first character of the first occurrence of @p needle.
*/
static char*
strnstr(const char *haystack, const char *needle, size_t len)
{
size_t i;
size_t needleLen;
const char* needleEnd;
// strnlen is not part of the C standard and does not compile on some platforms,
// use case is covered by memchr.
needleEnd = (char *)memchr(needle, 0, len);
if (needleEnd == needle)
return (char *)haystack;
needleLen = len;
if (needleEnd != NULL)
needleLen = needleEnd - needle;
for (i = 0; i <= len - needleLen; i++)
{
if (haystack[0] == needle[0]
&& strncmp(haystack, needle, needleLen) == 0)
return (char *)haystack;
haystack++;
}
return NULL;
}
#endif
/** @internal
* @~English
* @brief Append the library's id to the KTXwriter value.
*
* @param[in] head pointer to the head of the hash list.
* @param[in] writerEntry pointer to an existing KTXwriter entry.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_OUT_OF_MEMORY not enough memory for temporary strings.
* @exception KTX_INVALID_OPERATION
* the length of the value of writerEntry and the
* lib id being added is greater than the
* maximum allowed.
*/
KTX_error_code
appendLibId(ktxHashList* head, ktxHashListEntry* writerEntry)
{
KTX_error_code result;
const char* id;
const char* libVer;
const char libIdIntro[] = " / libktx ";
size_t idLen, libIdLen;
if (writerEntry) {
ktx_uint32_t len;
result = ktxHashListEntry_GetValue(writerEntry, &len, (void**)&id);
idLen = len;
} else {
id = "Unidentified app";
idLen = 17;
}
// strnstr needed because KTXwriter values may not be NUL terminated.
#if defined(EMPTY_LIBVER_WITH_UNIDENTIFIED_APP)
// May be needed for patching some CTS files without changing their KTXwriter
// metadata. Keep in case useful again.
if (strnstr(id, "Unidentified app", idLen) != NULL) {
libVer = "";
} else
#endif
if (strnstr(id, "__default__", idLen) != NULL) {
libVer = STR(LIBKTX_DEFAULT_VERSION);
} else {
libVer = STR(LIBKTX_VERSION);
}
// sizeof(libIdIntro) includes space for its terminating NUL which we will
// overwrite so no need for +1 after strlen.
libIdLen = sizeof(libIdIntro) + (ktx_uint32_t)strlen(libVer);
char* libId = malloc(libIdLen);
if (!libId)
return KTX_OUT_OF_MEMORY;
strncpy(libId, libIdIntro, libIdLen);
strncpy(&libId[sizeof(libIdIntro)-1], libVer,
libIdLen-(sizeof(libIdIntro)-1));
char* fullId = NULL;
if (strnstr(id, libId, idLen) != NULL) {
// This lib id is already in the writer value.
result = KTX_SUCCESS;
goto cleanup;
}
const char* libVerPos = strnstr(id, libIdIntro, idLen);
if (libVerPos != NULL) {
// There is a libktx version but not the current version.
idLen = libVerPos - id;
} else if (id[idLen-1] == '\0') {
idLen--;
}
size_t fullIdLen = idLen + strlen(libId) + 1;
if (fullIdLen > UINT_MAX) {
result = KTX_INVALID_OPERATION;
goto cleanup;
}
fullId = malloc(fullIdLen);
if (!fullId) {
result = KTX_OUT_OF_MEMORY;
goto cleanup;
}
strncpy(fullId, id, idLen);
strncpy(&fullId[idLen], libId, libIdLen);
assert(fullId[fullIdLen-1] == '\0');
ktxHashList_DeleteEntry(head, writerEntry);
result = ktxHashList_AddKVPair(head, KTX_WRITER_KEY,
(ktx_uint32_t)fullIdLen, fullId);
cleanup:
free(libId);
free(fullId);
return result;
}
/**
* @memberof ktxTexture2 @private
* @~English
* @brief Set image for level, layer, faceSlice from a ktxStream source.
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in] level mip level of the image to set.
* @param[in] layer array layer of the image to set.
* @param[in] faceSlice cube map face or depth slice of the image to set or
* KTX_FACESLICE_WHOLE_LEVEL to set the entire level.
* @param[in] src ktxStream pointer to the source.
* @param[in] srcSize size of the source image in bytes.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This or @p src is NULL.
* @exception KTX_INVALID_VALUE @p srcSize != the expected image size for the
* specified level, layer & faceSlice.
* @exception KTX_INVALID_OPERATION
* No storage was allocated when the texture was
* created.
*/
KTX_error_code
ktxTexture2_setImageFromStream(ktxTexture2* This, ktx_uint32_t level,
ktx_uint32_t layer, ktx_uint32_t faceSlice,
ktxStream* src, ktx_size_t srcSize)
{
ktx_size_t imageByteLength;
ktx_size_t imageByteOffset;
ktx_error_code_e result;
if (!This || !src)
return KTX_INVALID_VALUE;
if (!This->pData)
return KTX_INVALID_OPERATION;
if (faceSlice == KTX_FACESLICE_WHOLE_LEVEL) {
result = ktxTexture_GetImageOffset(ktxTexture(This), level, layer, 0, &imageByteOffset);
if (result != KTX_SUCCESS) {
return result;
}
imageByteLength = ktxTexture_calcLevelSize(ktxTexture(This), level, KTX_FORMAT_VERSION_TWO);
} else {
result = ktxTexture_GetImageOffset(ktxTexture(This), level, layer, faceSlice, &imageByteOffset);
if (result != KTX_SUCCESS) {
return result;
}
imageByteLength = ktxTexture_GetImageSize(ktxTexture(This), level);
}
if (srcSize != imageByteLength)
return KTX_INVALID_OPERATION;
// The above will catch a flagrantly invalid srcSize. This is an
// additional check of the internal calculations.
assert (imageByteOffset + srcSize <= This->dataSize);
/* Can copy whole image at once */
src->read(src, This->pData + imageByteOffset, srcSize);
return KTX_SUCCESS;
}
/**
* @memberof ktxTexture2
* @~English
* @brief Set image for level, layer, faceSlice from a stdio stream source.
*
* Uncompressed images read from the stream are expected to have their rows
* tightly packed as is the norm for most image file formats. KTX 2 also requires
* tight packing this function does not add any padding.
*
* Level, layer, faceSlice rather than offset are specified to enable some
* validation.
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in] level mip level of the image to set.
* @param[in] layer array layer of the image to set.
* @param[in] faceSlice cube map face or depth slice of the image to set or
* KTX_FACESLICE_WHOLE_LEVEL to set the entire level.
* @param[in] src stdio stream pointer to the source.
* @param[in] srcSize size of the source image in bytes.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This or @p src is NULL.
* @exception KTX_INVALID_VALUE @p srcSize != the expected image size for the
* specified level, layer & faceSlice.
* @exception KTX_INVALID_OPERATION
* No storage was allocated when the texture was
* created.
*/
KTX_error_code
ktxTexture2_SetImageFromStdioStream(ktxTexture2* This, ktx_uint32_t level,
ktx_uint32_t layer, ktx_uint32_t faceSlice,
FILE* src, ktx_size_t srcSize)
{
ktxStream srcstr;
KTX_error_code result;
result = ktxFileStream_construct(&srcstr, src, KTX_FALSE);
if (result != KTX_SUCCESS)
return result;
result = ktxTexture2_setImageFromStream(This, level, layer, faceSlice,
&srcstr, srcSize);
ktxFileStream_destruct(&srcstr);
return result;
}
/**
* @memberof ktxTexture2
* @~English
* @brief Set image for level, layer, faceSlice from an image in memory.
*
* Uncompressed images in memory are expected to have their rows tightly packed
* as is the norm for most image file formats. KTX 2 also requires
* tight packing this function does not add any padding.
*
* Level, layer, faceSlice rather than offset are specified to enable some
* validation.
*
* @note The caller is responsible for freeing the original image memory
* referred to by @p src.
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in] level mip level of the image to set.
* @param[in] layer array layer of the image to set.
* @param[in] faceSlice cube map face or depth slice of the image to set or
* KTX_FACESLICE_WHOLE_LEVEL to set the entire level.
* @param[in] src pointer to the image source in memory.
* @param[in] srcSize size of the source image in bytes.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This or @p src is NULL.
* @exception KTX_INVALID_VALUE @p srcSize != the expected image size for the
* specified level, layer & faceSlice.
* @exception KTX_INVALID_OPERATION
* No storage was allocated when the texture was
* created.
*/
KTX_error_code
ktxTexture2_SetImageFromMemory(ktxTexture2* This, ktx_uint32_t level,
ktx_uint32_t layer, ktx_uint32_t faceSlice,
const ktx_uint8_t* src, ktx_size_t srcSize)
{
ktxStream srcstr;
KTX_error_code result;
result = ktxMemStream_construct_ro(&srcstr, src, srcSize);
if (result != KTX_SUCCESS)
return result;
result = ktxTexture2_setImageFromStream(This, level, layer, faceSlice,
&srcstr, srcSize);
ktxMemStream_destruct(&srcstr);
return result;
}
#if defined(TestNoMetadata)
// Only so texturetests can test loading of files without any metadata.
ktx_bool_t __disableWriterMetadata__ = KTX_FALSE;
#endif
/**
* @memberof ktxTexture2
* @~English
* @brief Write a ktxTexture object to a ktxStream in KTX format.
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in] dststr destination ktxStream.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This or @p dststr is NULL.
* @exception KTX_INVALID_OPERATION
* The ktxTexture does not contain any image data.
* @exception KTX_INVALID_OPERATION
* Both kvDataHead and kvData are set in the
* ktxTexture
* @exception KTX_INVALID_OPERATION
* The length of the already set writerId metadata
* plus the library's version id exceeds the
* maximum allowed.
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
* the system.
* @exception KTX_FILE_WRITE_ERROR
* An error occurred while writing the file.
*/
KTX_error_code
ktxTexture2_WriteToStream(ktxTexture2* This, ktxStream* dststr)
{
DECLARE_PRIVATE(ktxTexture2);
KTX_header2 header = { .identifier = KTX2_IDENTIFIER_REF };
KTX_error_code result;
ktx_uint32_t kvdLen;
ktx_uint8_t* pKvd;
ktx_uint32_t align8PadLen = 0;
ktx_uint64_t sgdLen;
ktx_uint32_t initialLevelPadLen;
ktx_uint32_t levelIndexSize;
ktx_uint64_t baseOffset;
if (!dststr) {
return KTX_INVALID_VALUE;
}
if (This->pData == NULL)
return KTX_INVALID_OPERATION;
header.vkFormat = This->vkFormat;
header.typeSize = This->_protected->_typeSize;
header.pixelWidth = This->baseWidth;
header.pixelHeight = This->numDimensions > 1 ? This->baseHeight : 0;
header.pixelDepth = This->numDimensions > 2 ? This->baseDepth : 0;
header.layerCount = This->isArray ? This->numLayers : 0;
assert (This->isCubemap ? This->numFaces == 6 : This->numFaces == 1);
header.faceCount = This->numFaces;
assert (This->generateMipmaps? This->numLevels == 1 : This->numLevels >= 1);
header.levelCount = This->generateMipmaps ? 0 : This->numLevels;
header.supercompressionScheme = This->supercompressionScheme;
levelIndexSize = sizeof(ktxLevelIndexEntry) * This->numLevels;
baseOffset = sizeof(header) + levelIndexSize;
header.dataFormatDescriptor.byteOffset = (uint32_t)baseOffset;
header.dataFormatDescriptor.byteLength = *This->pDfd;
baseOffset += header.dataFormatDescriptor.byteLength;
ktxHashListEntry* pEntry;
// Check for invalid metadata.
for (pEntry = This->kvDataHead; pEntry != NULL; pEntry = ktxHashList_Next(pEntry)) {
unsigned int keyLen;
char* key;
ktxHashListEntry_GetKey(pEntry, &keyLen, &key);
if (strncasecmp(key, "KTX", 3) == 0) {
ktx_uint32_t i;
const char* knownKeys[] = {
"KTXcubemapIncomplete",
"KTXorientation",
"KTXglFormat",
"KTXdxgiFormat__",
"KTXmetalPixelFormat",
"KTXswizzle",
"KTXwriter",
"KTXwriterScParams",
"KTXastcDecodeMode",
"KTXanimData"
};
if (strncmp(key, "ktx", 3) == 0)
return KTX_INVALID_OPERATION;
// Check for unrecognized KTX keys.
for (i = 0; i < sizeof(knownKeys)/sizeof(char*); i++) {
if (strcmp(key, knownKeys[i]) == 0)
break;
}
if (i == sizeof(knownKeys)/sizeof(char*))
return KTX_INVALID_OPERATION;
}
}
#if defined(TestNoMetadata)
if (!__disableWriterMetadata__) {
#endif
pEntry = NULL;
result = ktxHashList_FindEntry(&This->kvDataHead, KTX_WRITER_KEY,
&pEntry);
result = appendLibId(&This->kvDataHead, pEntry);
if (result != KTX_SUCCESS)
return result;
#if defined(TestNoMetadata)
}
#endif
ktxHashList_Sort(&This->kvDataHead); // KTX2 requires sorted metadata.
ktxHashList_Serialize(&This->kvDataHead, &kvdLen, &pKvd);
header.keyValueData.byteOffset = kvdLen != 0 ? (uint32_t)baseOffset : 0;
header.keyValueData.byteLength = kvdLen;
baseOffset += kvdLen;
sgdLen = private->_sgdByteLength;
if (sgdLen) {
align8PadLen = _KTX_PAD8_LEN(baseOffset);
baseOffset += align8PadLen;
}
header.supercompressionGlobalData.byteOffset = sgdLen != 0 ? baseOffset : 0;
header.supercompressionGlobalData.byteLength = sgdLen;
baseOffset += sgdLen;
initialLevelPadLen = _KTX_PADN_LEN(This->_private->_requiredLevelAlignment,
baseOffset);
baseOffset += initialLevelPadLen;
// write header and indices
result = dststr->write(dststr, &header, sizeof(header), 1);
if (result != KTX_SUCCESS)
return result;
// Create a copy of the level index with file-adjusted offsets and write it.
ktxLevelIndexEntry* levelIndex
= (ktxLevelIndexEntry*)malloc(levelIndexSize);
if (!levelIndex)
return KTX_OUT_OF_MEMORY;
for (ktx_uint32_t level = 0; level < This->numLevels; level++) {
levelIndex[level].byteLength = private->_levelIndex[level].byteLength;
levelIndex[level].uncompressedByteLength
= private->_levelIndex[level].uncompressedByteLength;
levelIndex[level].byteOffset = private->_levelIndex[level].byteOffset;
levelIndex[level].byteOffset += baseOffset;
}
result = dststr->write(dststr, levelIndex, levelIndexSize, 1);
free(levelIndex);
if (result != KTX_SUCCESS)
return result;
// write data format descriptor
result = dststr->write(dststr, This->pDfd, 1, *This->pDfd);
// write keyValueData
if (kvdLen != 0) {
assert(pKvd != NULL);
result = dststr->write(dststr, pKvd, 1, kvdLen);
free(pKvd);
if (result != KTX_SUCCESS) {
return result;
}
}
char padding[32] = { 0 };
// write supercompressionGlobalData & sgdPadding
if (private->_sgdByteLength != 0) {
if (align8PadLen) {
result = dststr->write(dststr, padding, 1, align8PadLen);
if (result != KTX_SUCCESS) {
return result;
}
}
result = dststr->write(dststr, private->_supercompressionGlobalData,
1, private->_sgdByteLength);
if (result != KTX_SUCCESS) {
return result;
}
}
if (initialLevelPadLen) {
result = dststr->write(dststr, padding, 1, initialLevelPadLen);
if (result != KTX_SUCCESS) {
return result;
}
}
// write the image data
for (ktx_int32_t level = This->numLevels-1; level >= 0 && result == KTX_SUCCESS; --level)
{
ktx_uint64_t srcLevelOffset, levelSize;
#define DUMP_IMAGE 0
#if defined(DEBUG) || DUMP_IMAGE
ktx_size_t pos;
#endif
#if defined(DEBUG)
result = dststr->getpos(dststr, (ktx_off_t*)&pos);
// Could fail if stdout is a pipe
if (result == KTX_SUCCESS)
assert(pos == private->_levelIndex[level].byteOffset + baseOffset);
else
assert(result == KTX_FILE_ISPIPE);
#endif
srcLevelOffset = ktxTexture2_levelDataOffset(This, level);
levelSize = private->_levelIndex[level].byteLength;
#if DUMP_IMAGE
if (!This->isCompressed) {
for (layer = 0; layer < This->numLayers; layer++) {
ktx_uint32_t faceSlice;
for (faceSlice = 0; faceSlice < numImages; faceSlice++) {
dststr->getsize(dststr, &pos);
fprintf(stdout, "Writing level %d, layer %d, faceSlice %d to baseOffset %#zx\n",
level, layer, faceSlice, pos);
for (uint32_t y = 0; y < (This->baseHeight >> level); y++) {
for (uint32_t x = 0; x < rowBytes; x++) {
fprintf(stdout, "%#x, ",
*(This->pData + srcOffset + y * rowBytes + x));
}
fprintf(stdout, "\n");
}
}
}
}
fprintf(stdout, "\n");
#endif
// Write entire level.
result = dststr->write(dststr, This->pData + srcLevelOffset,
levelSize, 1);
if (result == KTX_SUCCESS && level > 0) { // No padding at end.
ktx_uint32_t levelPadLen
= _KTX_PADN_LEN(This->_private->_requiredLevelAlignment,
levelSize);
if (levelPadLen != 0)
result = dststr->write(dststr, padding, 1, levelPadLen);
}
}
return result;
}
/**
* @memberof ktxTexture2
* @~English
* @brief Write a ktxTexture object to a stdio stream in KTX format.
*
* Callers are strongly urged to include a KTXwriter item in the texture's
* metadata. It can be added by code, similar to the following, prior to
* calling this function.
* @code
* char writer[100];
* snprintf(writer, sizeof(writer), "%s version %s", appName, appVer);
* ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
* (ktx_uint32_t)strlen(writer) + 1,
* writer);
* @endcode
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in] dstsstr destination stdio stream.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This or @p dstsstr is NULL.
* @exception KTX_INVALID_OPERATION
* The ktxTexture does not contain any image data.
* @exception KTX_INVALID_OPERATION
* Both kvDataHead and kvData are set in the
* ktxTexture
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
* the system.
* @exception KTX_FILE_WRITE_ERROR
* An error occurred while writing the file.
*/
KTX_error_code
ktxTexture2_WriteToStdioStream(ktxTexture2* This, FILE* dstsstr)
{
ktxStream stream;
KTX_error_code result = KTX_SUCCESS;
if (!This)
return KTX_INVALID_VALUE;
result = ktxFileStream_construct(&stream, dstsstr, KTX_FALSE);
if (result != KTX_SUCCESS)
return result;
return ktxTexture2_WriteToStream(This, &stream);
}
/**
* @memberof ktxTexture2
* @~English
* @brief Write a ktxTexture object to a named file in KTX format.
*
* The file name must be encoded in utf-8. On Windows convert unicode names
* to utf-8 with @c WideCharToMultiByte(CP_UTF8, ...) before calling.
*
* Callers are strongly urged to include a KTXwriter item in the texture's
* metadata. It can be added by code, similar to the following, prior to
* calling this function.
* @code
* char writer[100];
* snprintf(writer, sizeof(writer), "%s version %s", appName, appVer);
* ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
* (ktx_uint32_t)strlen(writer) + 1,
* writer);
* @endcode
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in] dstname destination file name.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This or @p dstname is NULL.
* @exception KTX_INVALID_OPERATION
* The ktxTexture does not contain any image data.
* @exception KTX_INVALID_OPERATION
* Both kvDataHead and kvData are set in the
* ktxTexture
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
* the system.
* @exception KTX_FILE_WRITE_ERROR
* An error occurred while writing the file.
*/
KTX_error_code
ktxTexture2_WriteToNamedFile(ktxTexture2* This, const char* const dstname)
{
KTX_error_code result;
FILE* dst;
if (!This)
return KTX_INVALID_VALUE;
dst = ktxFOpenUTF8(dstname, "wb");
if (dst) {
result = ktxTexture2_WriteToStdioStream(This, dst);
fclose(dst);
} else
result = KTX_FILE_OPEN_FAILED;
return result;
}
/**
* @memberof ktxTexture2
* @~English
* @brief Write a ktxTexture object to block of memory in KTX format.
*
* Memory is allocated by the function and the caller is responsible for
* freeing it.
*
* Callers are strongly urged to include a KTXwriter item in the texture's
* metadata. It can be added by code, similar to the following, prior to
* calling this function.
* @code
* char writer[100];
* snprintf(writer, sizeof(writer), "%s version %s", appName, appVer);
* ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
* (ktx_uint32_t)strlen(writer) + 1,
* writer);
* @endcode
*
* @param[in] This pointer to the target ktxTexture object.
* @param[in,out] ppDstBytes pointer to location to write the address of
* the destination memory. The Application is
* responsible for freeing this memory.
* @param[in,out] pSize pointer to location to write the size in bytes of
* the KTX data.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_VALUE @p This, @p ppDstBytes or @p pSize is NULL.
* @exception KTX_INVALID_OPERATION
* The ktxTexture does not contain any image data.
* @exception KTX_INVALID_OPERATION
* Both kvDataHead and kvData are set in the
* ktxTexture
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
* the system.
* @exception KTX_FILE_WRITE_ERROR
* An error occurred while writing the file.
*/
KTX_error_code
ktxTexture2_WriteToMemory(ktxTexture2* This,
ktx_uint8_t** ppDstBytes, ktx_size_t* pSize)
{
struct ktxStream dststr;
KTX_error_code result;
ktx_size_t strSize;
if (!This || !ppDstBytes || !pSize)
return KTX_INVALID_VALUE;
*ppDstBytes = NULL;
result = ktxMemStream_construct(&dststr, KTX_FALSE);
if (result != KTX_SUCCESS)
return result;
result = ktxTexture2_WriteToStream(This, &dststr);
if(result != KTX_SUCCESS)
{
ktxMemStream_destruct(&dststr);
return result;
}
ktxMemStream_getdata(&dststr, ppDstBytes);
dststr.getsize(&dststr, &strSize);
*pSize = (GLsizei)strSize;
/* This function does not free the memory pointed at by the
* value obtained from ktxMemStream_getdata() thanks to the
* KTX_FALSE passed to the constructor above.
*/
ktxMemStream_destruct(&dststr);
return KTX_SUCCESS;
}
/**
* @memberof ktxTexture2
* @~English
* @brief Deflate the data in a ktxTexture2 object using Zstandard.
*
* The texture's levelIndex, dataSize, DFD, data pointer, and supercompressionScheme will
* all be updated after successful deflation to reflect the deflated data.
*
* @param[in] This pointer to the ktxTexture2 object of interest.
* @param[in] compressionLevel set speed vs compression ratio trade-off. Values
* between 1 and 22 are accepted. The lower the level the faster. Values
* above 20 should be used with caution as they require more memory.
*/
KTX_error_code
ktxTexture2_DeflateZstd(ktxTexture2* This, ktx_uint32_t compressionLevel)
{
ktx_uint32_t levelIndexByteLength =
This->numLevels * sizeof(ktxLevelIndexEntry);
ktx_uint8_t* workBuf;
ktx_uint8_t* cmpData;
ktx_size_t dstRemainingByteLength = 0;
ktx_size_t byteLengthCmp = 0;
ktx_size_t levelOffset = 0;
ktxLevelIndexEntry* cindex = This->_private->_levelIndex;
ktxLevelIndexEntry* nindex;
ktx_uint8_t* pCmpDst;
ktx_error_code_e result;
ZSTD_CCtx* cctx = ZSTD_createCCtx();
if (cctx == NULL)
return KTX_OUT_OF_MEMORY;
if (This->supercompressionScheme != KTX_SS_NONE)
return KTX_INVALID_OPERATION;
// On rare occasions the deflated data can be a few bytes larger than
// the source data. Calculating the dst buffer size using
// ZSTD_compressBound provides a suitable size plus compression is said
// to run faster when the dst buffer is >= compressBound.
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
dstRemainingByteLength += ZSTD_compressBound(cindex[level].byteLength);
}
workBuf = malloc(dstRemainingByteLength + levelIndexByteLength);
if (workBuf == NULL) {
result = KTX_OUT_OF_MEMORY;
goto cleanup;
}
nindex = (ktxLevelIndexEntry*)workBuf;
pCmpDst = &workBuf[levelIndexByteLength];
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
size_t levelByteLengthCmp =
ZSTD_compressCCtx(cctx, pCmpDst + levelOffset,
dstRemainingByteLength,
&This->pData[cindex[level].byteOffset],
cindex[level].byteLength,
compressionLevel);
if (ZSTD_isError(levelByteLengthCmp)) {
free(workBuf);
ZSTD_ErrorCode error = ZSTD_getErrorCode(levelByteLengthCmp);
switch(error) {
case ZSTD_error_parameter_outOfBound:
result = KTX_INVALID_VALUE;
goto cleanup;
case ZSTD_error_dstSize_tooSmall:
#ifdef DEBUG
assert(false && "Deflate dstSize too small.");
#else
result = KTX_OUT_OF_MEMORY;
goto cleanup;
#endif
case ZSTD_error_workSpace_tooSmall:
#ifdef DEBUG
assert(false && "Deflate workspace too small.");
#else
result = KTX_OUT_OF_MEMORY;
goto cleanup;
#endif
case ZSTD_error_memory_allocation:
result = KTX_OUT_OF_MEMORY;
goto cleanup;
default:
// The remaining errors look like they should only
// occur during decompression but just in case.
#ifdef DEBUG
assert(true);
#else
result = KTX_INVALID_OPERATION;
goto cleanup;
#endif
}
}
nindex[level].byteOffset = levelOffset;
nindex[level].uncompressedByteLength = cindex[level].byteLength;
nindex[level].byteLength = levelByteLengthCmp;
byteLengthCmp += levelByteLengthCmp;
levelOffset += levelByteLengthCmp;
dstRemainingByteLength -= levelByteLengthCmp;
}
ZSTD_freeCCtx(cctx);
// Move the compressed data into a correctly sized buffer.
cmpData = malloc(byteLengthCmp);
if (cmpData == NULL) {
free(workBuf);
return KTX_OUT_OF_MEMORY;
}
// Now modify the texture.
memcpy(cmpData, pCmpDst, byteLengthCmp); // Copy data to sized buffer.
memcpy(cindex, nindex, levelIndexByteLength); // Update level index
free(workBuf);
free(This->pData);
This->pData = cmpData;
This->dataSize = byteLengthCmp;
This->supercompressionScheme = KTX_SS_ZSTD;
This->_private->_requiredLevelAlignment = 1;
return KTX_SUCCESS;
cleanup:
ZSTD_freeCCtx(cctx);
free(workBuf);
return result;
}
/**
* @memberof ktxTexture2
* @~English
* @brief Deflate the data in a ktxTexture2 object using miniz (ZLIB).
*
* The texture's levelIndex, dataSize, DFD, data pointer, and supercompressionScheme will
* all be updated after successful deflation to reflect the deflated data.
*
* @param[in] This pointer to the ktxTexture2 object of interest.
* @param[in] compressionLevel set speed vs compression ratio trade-off. Values
* between 1 and 9 are accepted. The lower the level the faster.
*/
KTX_error_code
ktxTexture2_DeflateZLIB(ktxTexture2* This, ktx_uint32_t compressionLevel)
{
ktx_uint32_t levelIndexByteLength =
This->numLevels * sizeof(ktxLevelIndexEntry);
ktx_uint8_t* workBuf;
ktx_uint8_t* cmpData;
ktx_size_t dstRemainingByteLength = 0;
ktx_size_t byteLengthCmp = 0;
ktx_size_t levelOffset = 0;
ktxLevelIndexEntry* cindex = This->_private->_levelIndex;
ktxLevelIndexEntry* nindex;
ktx_uint8_t* pCmpDst;
if (This->supercompressionScheme != KTX_SS_NONE)
return KTX_INVALID_OPERATION;
// On rare occasions the deflated data can be a few bytes larger than
// the source data. Calculating the dst buffer size using
// mz_deflateBound provides a conservative size to account for that.
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
dstRemainingByteLength += ktxCompressZLIBBounds(cindex[level].byteLength);
}
workBuf = malloc(dstRemainingByteLength + levelIndexByteLength);
if (workBuf == NULL)
return KTX_OUT_OF_MEMORY;
nindex = (ktxLevelIndexEntry*)workBuf;
pCmpDst = &workBuf[levelIndexByteLength];
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
size_t levelByteLengthCmp = dstRemainingByteLength;
KTX_error_code result = ktxCompressZLIBInt(pCmpDst + levelOffset,
&levelByteLengthCmp,
&This->pData[cindex[level].byteOffset],
cindex[level].byteLength,
compressionLevel);
if (result != KTX_SUCCESS) {
free(workBuf);
return result;
}
nindex[level].byteOffset = levelOffset;
nindex[level].uncompressedByteLength = cindex[level].byteLength;
nindex[level].byteLength = levelByteLengthCmp;
byteLengthCmp += levelByteLengthCmp;
levelOffset += levelByteLengthCmp;
dstRemainingByteLength -= levelByteLengthCmp;
}
// Move the compressed data into a correctly sized buffer.
cmpData = malloc(byteLengthCmp);
if (cmpData == NULL) {
free(workBuf);
return KTX_OUT_OF_MEMORY;
}
// Now modify the texture.
memcpy(cmpData, pCmpDst, byteLengthCmp); // Copy data to sized buffer.
memcpy(cindex, nindex, levelIndexByteLength); // Update level index
free(workBuf);
free(This->pData);
This->pData = cmpData;
This->dataSize = byteLengthCmp;
This->supercompressionScheme = KTX_SS_ZLIB;
This->_private->_requiredLevelAlignment = 1;
return KTX_SUCCESS;
}
/** @} */