Files
KTX-Software/tools/ktx/command_create.cpp
Mark Callow bb7c7d1007 Support listing files for multiple inputs and u8string utf8 utilities. (#1103)
This is a piece of legacy tool functionality, used in our asset
generation script, that was missing.

This PR also adds a u8string version of the utf8 utilities in
platform_utils.h and u8string constructors to OutputStream{,Ex} to make
things work when building with c++20 and newer. All is protected by
macro checks for u8string support.
2026-01-03 17:15:49 +09:00

2950 lines
146 KiB
C++

// Copyright 2022-2023 The Khronos Group Inc.
// Copyright 2022-2023 RasterGrid Kft.
// SPDX-License-Identifier: Apache-2.0
#include "command.h"
#include "encode_utils_common.h"
#include "platform_utils.h"
#include "metrics_utils.h"
#include "deflate_utils.h"
#include "encode_utils_basis.h"
#include "encode_utils_astc.h"
#include "format_descriptor.h"
#include "formats.h"
#include "utility.h"
#include <filesystem>
#include <iostream>
#include <regex>
#include <sstream>
#include <cxxopts.hpp>
#include <fmt/ostream.h>
#include <fmt/printf.h>
#include "ktx.h"
#include "image.hpp"
#include "imageio.h"
/** @file
* @~English
* @brief @b create command implementation.
*/
// -------------------------------------------------------------------------------------------------
namespace ktx {
struct SrcColorSpaceInfo {
khr_df_transfer_e usedTransferFunction;
khr_df_primaries_e usedPrimaries;
std::unique_ptr<const TransferFunction> transferFunction{};
std::unique_ptr<const ColorPrimaries> colorPrimaries{};
};
struct DstColorSpaceInfo {
std::unique_ptr<const TransferFunction> transferFunction{};
std::unique_ptr<const ColorPrimaries> colorPrimaries{};
};
struct ColorSpaceInfo {
SrcColorSpaceInfo src;
DstColorSpaceInfo dst;
};
// -------------------------------------------------------------------------------------------------
struct OptionsCreate {
inline static const char* kFormat = "format";
inline static const char* k1D = "1d";
inline static const char* kCubemap = "cubemap";
inline static const char* kRaw = "raw";
inline static const char* kWidth = "width";
inline static const char* kHeight = "height";
inline static const char* kDepth = "depth";
inline static const char* kLayers = "layers";
inline static const char* kLevels = "levels";
inline static const char* kRuntimeMipmap = "runtime-mipmap";
inline static const char* kGenerateMipmap = "generate-mipmap";
inline static const char* kEncode = "encode";
inline static const char* kNormalize = "normalize";
inline static const char* kSwizzle = "swizzle";
inline static const char* kInputSwizzle = "input-swizzle";
inline static const char* kAssignOetf = "assign-oetf";
inline static const char* kAssignTf = "assign-tf";
inline static const char* kAssignPrimaries = "assign-primaries";
inline static const char* kAssignTexcoordOrigin = "assign-texcoord-origin";
inline static const char* kConvertOetf = "convert-oetf";
inline static const char* kConvertTf = "convert-tf";
inline static const char* kConvertPrimaries = "convert-primaries";
inline static const char* kConvertTexcoordOrigin = "convert-texcoord-origin";
inline static const char* kFailOnColorConversions = "fail-on-color-conversions";
inline static const char* kWarnOnColorConversions = "warn-on-color-conversions";
inline static const char* kNoWarnOnColorConversions = "no-warn-on-color-conversions";
inline static const char* kFailOnOriginChanges = "fail-on-origin-changes";
inline static const char* kWarnOnOriginChanges = "warn-on-origin-changes";
inline static const char* kMipmapFilter = "mipmap-filter";
inline static const char* kMipmapFilterScale = "mipmap-filter-scale";
inline static const char* kMipmapWrap = "mipmap-wrap";
inline static const char* kScale = "scale";
inline static const char* kPremultiplyAlpha = "premultiply-alpha";
bool _1d = false;
bool cubemap = false;
VkFormat vkFormat = VK_FORMAT_UNDEFINED;
FormatDescriptor formatDesc;
bool raw = false;
std::optional<uint32_t> width;
std::optional<uint32_t> height;
std::optional<uint32_t> depth;
std::optional<uint32_t> layers;
std::optional<uint32_t> levels;
bool mipmapRuntime = false;
bool mipmapGenerate = false;
std::optional<std::string> mipmapFilter;
std::string defaultMipmapFilter = "lanczos4";
std::optional<float> mipmapFilterScale;
float defaultMipmapFilterScale = 1.0f;
std::optional<basisu::Resampler::Boundary_Op> mipmapWrap;
basisu::Resampler::Boundary_Op defaultMipmapWrap = basisu::Resampler::Boundary_Op::BOUNDARY_WRAP;
std::optional<std::string> swizzle; /// Sets KTXswizzle
std::optional<std::string> swizzleInput; /// Used to swizzle the input image data
std::optional<float> imageScale;
std::optional<khr_df_transfer_e> convertTF = {};
std::optional<khr_df_transfer_e> assignTF = {};
std::optional<khr_df_primaries_e> assignPrimaries = {};
std::optional<khr_df_primaries_e> convertPrimaries = {};
std::optional<ImageSpec::Origin> assignTexcoordOrigin;
std::optional<ImageSpec::Origin> convertTexcoordOrigin;
bool failOnColorConversions = false;
bool warnOnColorConversions = false;
bool noWarnOnColorConversions = false;
bool failOnOriginChanges = false;
bool warnOnOriginChanges = false;
bool normalize = false;
bool premultiplyAlpha = false;
void init(cxxopts::Options& opts) {
opts.add_options()
(kFormat, "KTX format enum that specifies the image data format."
" The enum names are matching the VkFormats without the VK_FORMAT_ prefix."
" The VK_FORMAT_ prefix is ignored if present."
"\nWhen used with --encode it specifies the target format before the encoding step."
" In this case it must be one of:"
"\n R8_UNORM"
"\n R8_SRGB"
"\n R8G8_UNORM"
"\n R8G8_SRGB"
"\n R8G8B8_UNORM"
"\n R8G8B8_SRGB"
"\n R8G8B8A8_UNORM"
"\n R8G8B8A8_SRGB"
"\nIf the format is an ASTC format the ASTC encoder specific options become valid,"
" otherwise they are ignored."
"\nThe format will be used to verify and load all input files into a texture before encoding."
" Case insensitive. Required.", cxxopts::value<std::string>(), "<enum>")
(k1D, "Create a 1D texture. If not set the texture will be a 2D or 3D texture.")
(kCubemap, "Create a cubemap texture. If not set the texture will be a 2D or 3D texture.")
(kRaw, "Create from raw image data.")
(kWidth, "Base level width in pixels. Required with --raw. For non-raw, if not set,"
" the image width is used otherwise the image is resampled to this width and"
" any provided mip levels are resampled proportionately. For non-raw it enables"
" use of the \'Generate Mipmap\' options to tune the resampler.",
cxxopts::value<uint32_t>(), "[0-9]+")
(kHeight, "Base level height in pixels. Required with --raw. For non-raw, if not"
" set, the image height is used otherwise the image is resampled to this height"
" and any provided mip levels are resampled proportionately. For non-raw it"
" enables use of the \'Generate Mipmap\' options to tune the resampler.",
cxxopts::value<uint32_t>(), "[0-9]+")
(kDepth, "Base level depth in pixels. If set the texture will be a 3D texture.", cxxopts::value<uint32_t>(), "[0-9]+")
(kLayers, "Number of layers. If set the texture will be an array texture.", cxxopts::value<uint32_t>(), "[0-9]+")
(kLevels, "Number of mip levels.", cxxopts::value<uint32_t>(), "[0-9]+")
(kRuntimeMipmap, "Runtime mipmap generation mode.")
(kGenerateMipmap, "Causes mipmaps to be generated during texture creation."
" It enables the use of \'Generate Mipmap\' options."
" If --levels is not specified the maximum possible mip level will be generated."
" This option is mutually exclusive with --runtime-mipmap and cannot be used with SINT,"
" UINT or 3D textures or --raw.")
(kScale, "Scale images as they are loaded. Cannot be used with --raw. It enables use of"
" the \'Generate Mipmap\' options to tune the resampler.",
cxxopts::value<float>(), "<float>")
(kEncode, "Encode the created KTX file. Case insensitive."
"\nPossible options are: basis-lz | uastc", cxxopts::value<std::string>(), "<codec>")
(kNormalize, "Normalize input normals to have a unit length. Only valid for\n"
"linear normal textures with 2 or more components. For 2-component\n"
"inputs 2D unit normals are calculated. Do not use these 2D unit\n"
"normals to generate X+Y normals with --normal-mode. For 4-component\n"
"inputs a 3D unit normal is calculated. 1.0 is used for the value of\n"
"the 4th component. Cannot be used with --raw.")
(kPremultiplyAlpha, "Pre-multiplies the color components of the input pixels by the alpha component"
" before encoding and sets the flag in the metadata. Cannot be used with --normalize or --raw.")
(kSwizzle, "KTX swizzle metadata.", cxxopts::value<std::string>(), "[rgba01]{4}")
(kInputSwizzle, "Pre-swizzle input channels.", cxxopts::value<std::string>(), "[rgba01]{4}")
(kAssignTf, "Force the created texture to have the specified transfer function, ignoring"
" the transfer function of the input file(s). Possible options match the khr_df_transfer_e"
" enumerators without the KHR_DF_TRANSFER_ prefix. The KHR_DF_TRANSFER_ prefix is ignored"
" if present. Case insensitive."
"\nThe options are:"
" linear | srgb | srgb_eotf | scrgb | scrgb_eotf | itu | itu_oetf | bt601 | bt601_oetf | bt709 | bt709_oetf |"
" bt2020 | bt2020_oetf | smpte170m | smpte170m_oetf | smpte170m_eotf | ntsc | ntsc_eotf | slog | slog_oetf |"
" slog2 | slog2_oetf | bt1886 | bt1886_eotf | hlg_oetf | hlg_eotf | pq_oetf | pg_eotf | dcip3 | dcip3_eotf |"
" pal_oetf | pal625_eotf | st240 | st240_oetf | st240_eotf | acescc | acescc_oetf | acescct | acescct_oetf |"
" abobergb | adobergb_eotf",
cxxopts::value<std::string>(), "<tf>")
(kAssignOetf, "Same as --assign-tf. Deprecated.", cxxopts::value<std::string>(), "<tf>")
(kAssignPrimaries, "Force the created texture to have the specified color primaries, ignoring"
" the color primaries of the input file(s). Possible options match the khr_df_primaries_e"
" enumerators without the KHR_DF_PRIMARIES_ prefix. The KHR_DF_PRIMARIES_ prefix is ignored"
" if present. Case insensitive."
"\nThe options are:"
" none | bt709 | srgb | bt601_ebu | bt601_smpte | bt2020 | ciexyz | aces | acescc | ntsc1953 | pal525 | displayp3 | adobergb.",
cxxopts::value<std::string>(), "<primaries>")
(kAssignTexcoordOrigin, "Force the created texture to indicate that the texture coordinate"
" origin s=0, t=0 is at the specified corner of the image. Case insensitive."
"\nPossible options are top-left | bottom-left. -front | -back can be appended and"
" one of these is required when --depth is specified. Must be top-left if --cubemap"
" is specified."
"\nAbsent --convert-texcoord-origin, the effect of this option is to cause KTXorientation"
" metadata indicating the specified origin to be written to the output file.",
cxxopts::value<std::string>(), "<origin>")
(kConvertTf, "Convert the input image(s) to the specified transfer function, if different"
" from the transfer function of the input file(s). If both this and --assign-tf are specified,"
" conversion will be performed from the assigned transfer function to the transfer function"
" specified by this option, if different. Case insensitive."
"\nPossible options are: linear | srgb. The following srgb aliases are also supported:"
" srgb_eotf | scrgb | scrgb_eotf",
cxxopts::value<std::string>(), "<tf>")
(kConvertOetf, "Same as --convert-tf. Deprecated.", cxxopts::value<std::string>(), "<tf>")
(kConvertPrimaries, "Convert the image image(s) to the specified color primaries, if different"
" from the color primaries of the input file(s) or the one specified by --assign-primaries."
" If both this and --assign-primaries are specified, conversion will be performed from "
" the assigned primaries to the primaries specified by this option, if different."
" This option is not allowed to be specified when --assign-primaries is set to 'none'."
" Possible options match the khr_df_transfer_e enumerators without the KHR_DF_TRANSFER_ prefix."
" The KHR_DF_PRIMARIES_ prefix is ignored if present. Case insensitive."
"\nThe options are:"
" bt709 | srgb | bt601_ebu | bt601_smpte | bt2020 | ciexyz | aces | acescc | ntsc1953 | pal525 | displayp3 | adobergb.",
cxxopts::value<std::string>(), "<primaries>")
(kConvertTexcoordOrigin, "Convert the input image(s) so the texture coordinate origin s=0,"
" t=0, is at the specified corner of the image. If both this and --assign-texcoord-origin"
" are specified, conversion will be performed from the assigned origin to the origin"
" specified by this option, if different. Case insensitive."
"\nPossible options are top-left | bottom-left. -front | -back can be appended and"
" one of these is required when --depth is specified. Must be top-left if --cubemap"
" is specified."
"\nInput images whose origin does not match corner will be flipped vertically."
" KTXorientation metadata indicating the specified origin is written to the output file.",
cxxopts::value<std::string>(), "<origin>")
(kFailOnColorConversions, "Generates an error if any of the input images would need to be color converted.")
(kWarnOnColorConversions, "Generates a warning if any of the input images are color converted.")
(kNoWarnOnColorConversions, "Disable all warnings about color conversions including that for"
" visually lossy conversions. Overrides --warn-on-color-conversions should both be specified.")
(kFailOnOriginChanges, "Generates an error if any of the input images would need to have their origin changed.")
(kWarnOnOriginChanges, "Generates a warning if any of the input images have their origin changed.");
opts.add_options("Generate Mipmap")
(kMipmapFilter, "Specifies the filter to use when generating the mipmaps. Case insensitive."
" Ignored unless --generate-mipmap, --scale, --width or --height are specified for"
" non-raw input."
"\nPossible options are:"
" box | tent | bell | b-spline | mitchell | blackman | lanczos3 | lanczos4 | lanczos6 |"
" lanczos12 | kaiser | gaussian | catmullrom | quadratic_interp | quadratic_approx | "
" quadratic_mix."
" Defaults to lanczos4.",
cxxopts::value<std::string>(), "<filter>")
(kMipmapFilterScale, "The filter scale to use. Defaults to 1.0. Ignored unless --generate-mipmap,"
" --scale, --width or --height are specified for non-raw input.",
cxxopts::value<float>(), "<float>")
(kMipmapWrap, "Specify how to sample pixels near the image boundaries. Case insensitive."
" Ignored unless --generate-mipmap, --scale, --width or --height are specified for"
" non-raw input."
"\nPossible options are:"
" wrap | reflect | clamp."
" Defaults to clamp.", cxxopts::value<std::string>(), "<mode>");
}
std::optional<khr_df_transfer_e> parseTransferFunction(cxxopts::ParseResult& args,
const char* argName,
const char* deprArgName,
Reporter& report) const {
// Many of these are aliases of others. To prevent breakage, in the
// unlikely event one is changed to not be an alias, the aliased
// enumerator names are used.
static const std::unordered_map<std::string, khr_df_transfer_e> values{
{ "NONE", KHR_DF_TRANSFER_UNSPECIFIED },
{ "LINEAR", KHR_DF_TRANSFER_LINEAR },
{ "SRGB", KHR_DF_TRANSFER_SRGB },
{ "SRGB_EOTF", KHR_DF_TRANSFER_SRGB_EOTF }, // SRGB
{ "SCRGB", KHR_DF_TRANSFER_SCRGB }, // SRGB
{ "SCRGB_EOTF", KHR_DF_TRANSFER_SRGB_EOTF }, // SRGB
{ "ITU", KHR_DF_TRANSFER_ITU },
{ "ITU_OETF", KHR_DF_TRANSFER_ITU_OETF }, // ITU
{ "BT601", KHR_DF_TRANSFER_BT601_OETF }, // ITU
{ "BT601_OETF", KHR_DF_TRANSFER_BT601_OETF }, // ITU
{ "BT709", KHR_DF_TRANSFER_BT709_OETF }, // ITU
{ "BT709_OETF", KHR_DF_TRANSFER_BT709_OETF }, // ITU
{ "BT2020", KHR_DF_TRANSFER_BT2020_OETF }, // ITU
{ "BT2020_OETF", KHR_DF_TRANSFER_BT2020_OETF }, // ITU
{ "SMPTE170M", KHR_DF_TRANSFER_SMTPE170M }, // ITU
{ "SMPTE170M_EOTF", KHR_DF_TRANSFER_SMTPE170M_EOTF }, // ITU
{ "SMPTE170M_OETF", KHR_DF_TRANSFER_SMTPE170M_OETF }, // ITU
{ "NTSC", KHR_DF_TRANSFER_NTSC },
{ "NTSC_EOTF", KHR_DF_TRANSFER_NTSC_EOTF }, // NTSC
{ "SLOG", KHR_DF_TRANSFER_SLOG },
{ "SLOG_OETF", KHR_DF_TRANSFER_SLOG_OETF }, // SLOG
{ "SLOG2", KHR_DF_TRANSFER_SLOG2 },
{ "SLOG2_OETF", KHR_DF_TRANSFER_SLOG2_OETF }, // SLOG2
{ "BT1886", KHR_DF_TRANSFER_BT1886 },
{ "BT1886_EOTF", KHR_DF_TRANSFER_BT1886_EOTF }, // BT1886
{ "HLG_OETF", KHR_DF_TRANSFER_HLG_OETF },
{ "HLG_EOTF", KHR_DF_TRANSFER_HLG_EOTF },
{ "PQ_OETF", KHR_DF_TRANSFER_PQ_OETF },
{ "PQ_EOTF", KHR_DF_TRANSFER_PQ_EOTF },
{ "DCIP3", KHR_DF_TRANSFER_DCIP3 },
{ "DCIP3_EOTF", KHR_DF_TRANSFER_DCIP3_EOTF }, // DCIP3
{ "PAL_OETF", KHR_DF_TRANSFER_PAL_OETF },
{ "PAL625_EOTF", KHR_DF_TRANSFER_PAL625_EOTF },
{ "ST240", KHR_DF_TRANSFER_ST240 },
{ "ST240_EOTF", KHR_DF_TRANSFER_ST240_EOTF }, // ST240
{ "ST240_OETF", KHR_DF_TRANSFER_ST240_OETF }, // ST240
{ "ACESCC", KHR_DF_TRANSFER_ACESCC },
{ "ACESCC_OETF", KHR_DF_TRANSFER_ACESCC_OETF }, // ACESCC
{ "ACESCCT", KHR_DF_TRANSFER_ACESCCT },
{ "ACESCCT_OETF", KHR_DF_TRANSFER_ACESCCT_OETF }, // ACESCCT
{ "ADOBERGB", KHR_DF_TRANSFER_ADOBERGB },
{ "ADOBERGB_EOTF", KHR_DF_TRANSFER_ADOBERGB_EOTF }, // ADOBERGB
{ "HLG_UNNORMALIZED_OETF", KHR_DF_TRANSFER_HLG_UNNORMALIZED_OETF },
};
std::optional<khr_df_transfer_e> result = {};
const char* argNameToUse = nullptr;
if (args[argName].count()) {
argNameToUse = argName;
} else if (args[deprArgName].count()) { // Prefer non-depcrecated name.
report.warning("Option --{} is deprecated and will be removed in the next release. Use --{} instead.",
deprArgName, argName);
argNameToUse = deprArgName;
}
if (argNameToUse) {
auto transferStr = to_upper_copy(args[argNameToUse].as<std::string>());
const std::string prefixStr = "KHR_DF_TRANSFER_";
if (transferStr.find(prefixStr) == 0) {
transferStr.erase(transferStr.begin(),
transferStr.begin() + prefixStr.size());
}
const auto it = values.find(transferStr);
if (it != values.end()) {
result = it->second;
} else {
report.fatal_usage("Invalid or unsupported transfer specified as --{} argument: \"{}\".",
argNameToUse, args[argNameToUse].as<std::string>());
}
}
return result;
}
std::optional<khr_df_primaries_e> parseColorPrimaries(cxxopts::ParseResult& args, const char* argName, Reporter& report) const {
static const std::unordered_map<std::string, khr_df_primaries_e> values{
{ "NONE", KHR_DF_PRIMARIES_UNSPECIFIED },
{ "BT709", KHR_DF_PRIMARIES_BT709 },
{ "SRGB", KHR_DF_PRIMARIES_SRGB },
{ "BT601_EBU", KHR_DF_PRIMARIES_BT601_EBU },
{ "BT601_SMPTE", KHR_DF_PRIMARIES_BT601_SMPTE },
{ "BT2020", KHR_DF_PRIMARIES_BT2020 },
{ "CIEXYZ", KHR_DF_PRIMARIES_CIEXYZ },
{ "ACES", KHR_DF_PRIMARIES_ACES },
{ "ACESCC", KHR_DF_PRIMARIES_ACESCC },
{ "NTSC1953", KHR_DF_PRIMARIES_NTSC1953 },
{ "PAL525", KHR_DF_PRIMARIES_PAL525 },
{ "DISPLAYP3", KHR_DF_PRIMARIES_DISPLAYP3 },
{ "ADOBERGB", KHR_DF_PRIMARIES_ADOBERGB },
};
std::optional<khr_df_primaries_e> result = {};
if (args[argName].count()) {
auto primariesStr = to_upper_copy(args[argName].as<std::string>());
const std::string prefixStr = "KHR_DF_PRIMARIES_";
if (primariesStr.find(prefixStr) == 0) {
primariesStr.erase(primariesStr.begin(),
primariesStr.begin() + prefixStr.size());
}
const auto it = values.find(primariesStr);
if (it != values.end()) {
result = it->second;
} else {
report.fatal_usage("Invalid or unsupported primaries specified as --{} argument: \"{}\".", argName,
args[argName].as<std::string>());
}
}
return result;
}
std::optional<ImageSpec::Origin> parseTexcoordOrigin(cxxopts::ParseResult& args, uint32_t numDimensions, const char* argName, Reporter& report) const {
std::optional<ImageSpec::Origin> result;
if (args[argName].count()) {
// RE to extract origin for each dimension.
// - Match 0 is whole matching string.
// - Match 1 is the y origin.
// - Match 2 is the x origin.
// - Match 3 is the z origin. Empty string, if not specified.
// Use raw literal to avoid excess blackslashes
std::regex re(R"--((?:\b(top|bottom)\b-)(?:\b(left)\b)(?:-\b(front|back)\b)?)--");
// For when support for right origin and 1d textures is added.
// y dimension made optional ꜜ right added ꜜ
//std::regex re(R"--((?:\b(top|bottom)\b-)?(?:\b(left|right)\b)(?:-\b(front|back)\b)?)--");
// "auto" here leads to no matching function call for regex_match.
const std::string& originStr = to_lower_copy(args[argName].as<std::string>());
std::smatch sm;
std::regex_match(originStr.begin(), originStr.end(), sm, re);
#if DEBUG_REGEX
std::cout << "match size: " << sm.size() << '\n';
for(uint32_t i = 0; i < sm.size(); i++) {
std::cout << "match " << i << ": " << "\"" << sm.str(i) << "\"" << '\n';
}
#endif
if (sm.empty()) {
report.fatal_usage("Invalid or unsupported origin specified as --{} argument: \"{}\".", argName, originStr);
}
if (numDimensions == 3 && sm.str(3).empty()) {
report.fatal_usage("Z origin must be specified in --{} argument for a 3D texture.", argName);
}
ImageSpec::Origin orig;
// Remember, compare returns 0 for a match.
orig.x = sm.str(2).compare("left") ? ImageSpec::Origin::eRight
: ImageSpec::Origin::eLeft;
orig.y = sm.str(1).compare("bottom") ? ImageSpec::Origin::eTop
: ImageSpec::Origin::eBottom;
if (args[kCubemap].count()) {
if (orig.x != ImageSpec::Origin::eLeft || orig.y != ImageSpec::Origin::eTop) {
report.fatal_usage("--{} argument must be top-left for a cubemap.", argName);
}
}
if (numDimensions == 3)
orig.z = sm.str(3).compare("front") ? ImageSpec::Origin::eFront
: ImageSpec::Origin::eBack;
result = std::move(orig);
}
return result;
}
void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) {
_1d = args[k1D].as<bool>();
cubemap = args[kCubemap].as<bool>();
raw = args[kRaw].as<bool>();
if (args[kWidth].count())
width = args[kWidth].as<uint32_t>();
if (args[kHeight].count())
height = args[kHeight].as<uint32_t>();
if (args[kDepth].count())
depth = args[kDepth].as<uint32_t>();
if (args[kLayers].count())
layers = args[kLayers].as<uint32_t>();
if (args[kLevels].count())
levels = args[kLevels].as<uint32_t>();
mipmapRuntime = args[kRuntimeMipmap].as<bool>();
mipmapGenerate = args[kGenerateMipmap].as<bool>();
if (args[kMipmapFilter].count()) {
static const std::unordered_set<std::string> filter_table{
"box",
"tent",
"bell",
"b-spline",
"mitchell",
"blackman",
"lanczos3",
"lanczos4",
"lanczos6",
"lanczos12",
"kaiser",
"gaussian",
"catmullrom",
"quadratic_interp",
"quadratic_approx",
"quadratic_mix",
};
mipmapFilter = to_lower_copy(args[kMipmapFilter].as<std::string>());
if (filter_table.count(*mipmapFilter) == 0)
report.fatal_usage("Invalid or unsupported mipmap filter specified as --mipmap-filter argument: \"{}\".", *mipmapFilter);
}
if (args[kMipmapFilterScale].count())
mipmapFilterScale = args[kMipmapFilterScale].as<float>();
if (args[kMipmapWrap].count()) {
static const std::unordered_map<std::string, basisu::Resampler::Boundary_Op> wrap_table{
{ "clamp", basisu::Resampler::Boundary_Op::BOUNDARY_CLAMP },
{ "wrap", basisu::Resampler::Boundary_Op::BOUNDARY_WRAP },
{ "reflect", basisu::Resampler::Boundary_Op::BOUNDARY_REFLECT },
};
const auto wrapStr = to_lower_copy(args[kMipmapWrap].as<std::string>());
const auto it = wrap_table.find(wrapStr);
if (it == wrap_table.end())
report.fatal_usage("Invalid or unsupported mipmap wrap mode specified as --mipmap-wrap argument: \"{}\".", wrapStr);
else
mipmapWrap = it->second;
}
if (args[kNormalize].count()) {
if (raw)
report.fatal_usage("Option --{} cannot be used with --{}.", kNormalize, kRaw);
normalize = true;
}
if (args[kSwizzle].count()) {
swizzle = to_lower_copy(args[kSwizzle].as<std::string>());
const auto errorFmt = "Invalid --swizzle value: \"{}\". The value must match the \"[rgba01]{{4}}\" regex.";
if (swizzle->size() != 4)
report.fatal_usage(errorFmt, *swizzle);
for (const auto c : *swizzle)
if (!contains("rgba01", c))
report.fatal_usage(errorFmt, *swizzle);
}
if (args[kInputSwizzle].count()) {
swizzleInput = to_lower_copy(args[kInputSwizzle].as<std::string>());
const auto errorFmt = "Invalid --input-swizzle value: \"{}\". The value must match the \"[rgba01]{{4}}\" regex.";
if (swizzleInput->size() != 4)
report.fatal_usage(errorFmt, *swizzleInput);
for (const auto c : *swizzleInput)
if (!contains("rgba01", c))
report.fatal_usage(errorFmt, *swizzleInput);
}
uint32_t numDimensions = 2;
if (args[kDepth].count())
numDimensions = 3;
else if (args[k1D].count())
numDimensions = 1;
assignTexcoordOrigin = parseTexcoordOrigin(args, numDimensions,
kAssignTexcoordOrigin, report);
convertTexcoordOrigin = parseTexcoordOrigin(args, numDimensions,
kConvertTexcoordOrigin, report);
if (args[kFormat].count()) {
const auto formatStr = args[kFormat].as<std::string>();
const auto parsedVkFormat = parseVkFormat(formatStr);
if (!parsedVkFormat)
report.fatal_usage("The requested format is invalid or unsupported: \"{}\".", formatStr);
vkFormat = *parsedVkFormat;
} else {
report.fatal_usage("Required option 'format' is missing.");
}
if (args[kScale].count()) {
if (args[kWidth].count() || args[kHeight].count())
report.fatal_usage("{} cannot be used with {} or {}.", kScale, kWidth, kHeight);
imageScale = args[kScale].as<float>();
}
// List of formats that have supported format conversions
static const std::unordered_set<VkFormat> convertableFormats{
VK_FORMAT_R8_UNORM,
VK_FORMAT_R8_SRGB,
VK_FORMAT_R8G8_UNORM,
VK_FORMAT_R8G8_SRGB,
VK_FORMAT_R8G8B8_UNORM,
VK_FORMAT_R8G8B8_SRGB,
VK_FORMAT_B8G8R8_UNORM,
VK_FORMAT_B8G8R8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_R8G8B8A8_SRGB,
VK_FORMAT_B8G8R8A8_UNORM,
VK_FORMAT_B8G8R8A8_SRGB,
VK_FORMAT_A8B8G8R8_UNORM_PACK32,
VK_FORMAT_A8B8G8R8_SRGB_PACK32,
VK_FORMAT_ASTC_4x4_UNORM_BLOCK,
VK_FORMAT_ASTC_4x4_SRGB_BLOCK,
VK_FORMAT_ASTC_5x4_UNORM_BLOCK,
VK_FORMAT_ASTC_5x4_SRGB_BLOCK,
VK_FORMAT_ASTC_5x5_UNORM_BLOCK,
VK_FORMAT_ASTC_5x5_SRGB_BLOCK,
VK_FORMAT_ASTC_6x5_UNORM_BLOCK,
VK_FORMAT_ASTC_6x5_SRGB_BLOCK,
VK_FORMAT_ASTC_6x6_UNORM_BLOCK,
VK_FORMAT_ASTC_6x6_SRGB_BLOCK,
VK_FORMAT_ASTC_8x5_UNORM_BLOCK,
VK_FORMAT_ASTC_8x5_SRGB_BLOCK,
VK_FORMAT_ASTC_8x6_UNORM_BLOCK,
VK_FORMAT_ASTC_8x6_SRGB_BLOCK,
VK_FORMAT_ASTC_8x8_UNORM_BLOCK,
VK_FORMAT_ASTC_8x8_SRGB_BLOCK,
VK_FORMAT_ASTC_10x5_UNORM_BLOCK,
VK_FORMAT_ASTC_10x5_SRGB_BLOCK,
VK_FORMAT_ASTC_10x6_UNORM_BLOCK,
VK_FORMAT_ASTC_10x6_SRGB_BLOCK,
VK_FORMAT_ASTC_10x8_UNORM_BLOCK,
VK_FORMAT_ASTC_10x8_SRGB_BLOCK,
VK_FORMAT_ASTC_10x10_UNORM_BLOCK,
VK_FORMAT_ASTC_10x10_SRGB_BLOCK,
VK_FORMAT_ASTC_12x10_UNORM_BLOCK,
VK_FORMAT_ASTC_12x10_SRGB_BLOCK,
VK_FORMAT_ASTC_12x12_UNORM_BLOCK,
VK_FORMAT_ASTC_12x12_SRGB_BLOCK,
VK_FORMAT_R4G4_UNORM_PACK8,
VK_FORMAT_R5G6B5_UNORM_PACK16,
VK_FORMAT_B5G6R5_UNORM_PACK16,
VK_FORMAT_R4G4B4A4_UNORM_PACK16,
VK_FORMAT_B4G4R4A4_UNORM_PACK16,
VK_FORMAT_R5G5B5A1_UNORM_PACK16,
VK_FORMAT_B5G5R5A1_UNORM_PACK16,
VK_FORMAT_A1R5G5B5_UNORM_PACK16,
VK_FORMAT_A4R4G4B4_UNORM_PACK16,
VK_FORMAT_A4B4G4R4_UNORM_PACK16,
VK_FORMAT_R10X6_UNORM_PACK16,
VK_FORMAT_R10X6G10X6_UNORM_2PACK16,
VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16,
VK_FORMAT_R12X4_UNORM_PACK16,
VK_FORMAT_R12X4G12X4_UNORM_2PACK16,
VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16,
VK_FORMAT_R16_UNORM,
VK_FORMAT_R16G16_UNORM,
VK_FORMAT_R16G16B16_UNORM,
VK_FORMAT_R16G16B16A16_UNORM,
VK_FORMAT_A2R10G10B10_UNORM_PACK32,
VK_FORMAT_A2B10G10R10_UNORM_PACK32,
VK_FORMAT_G8B8G8R8_422_UNORM,
VK_FORMAT_B8G8R8G8_422_UNORM,
VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16,
VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16,
VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16,
VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16,
VK_FORMAT_G16B16G16R16_422_UNORM,
VK_FORMAT_B16G16R16G16_422_UNORM,
VK_FORMAT_R8_UINT,
VK_FORMAT_R8_SINT,
VK_FORMAT_R16_UINT,
VK_FORMAT_R16_SINT,
VK_FORMAT_R32_UINT,
VK_FORMAT_R8G8_UINT,
VK_FORMAT_R8G8_SINT,
VK_FORMAT_R16G16_UINT,
VK_FORMAT_R16G16_SINT,
VK_FORMAT_R32G32_UINT,
VK_FORMAT_R8G8B8_UINT,
VK_FORMAT_R8G8B8_SINT,
VK_FORMAT_B8G8R8_UINT,
VK_FORMAT_B8G8R8_SINT,
VK_FORMAT_R16G16B16_UINT,
VK_FORMAT_R16G16B16_SINT,
VK_FORMAT_R32G32B32_UINT,
VK_FORMAT_R8G8B8A8_UINT,
VK_FORMAT_R8G8B8A8_SINT,
VK_FORMAT_B8G8R8A8_UINT,
VK_FORMAT_B8G8R8A8_SINT,
VK_FORMAT_A8B8G8R8_UINT_PACK32,
VK_FORMAT_A8B8G8R8_SINT_PACK32,
VK_FORMAT_R16G16B16A16_UINT,
VK_FORMAT_R16G16B16A16_SINT,
VK_FORMAT_R32G32B32A32_UINT,
VK_FORMAT_A2R10G10B10_UINT_PACK32,
VK_FORMAT_A2R10G10B10_SINT_PACK32,
VK_FORMAT_A2B10G10R10_SINT_PACK32,
VK_FORMAT_A2B10G10R10_UINT_PACK32,
VK_FORMAT_R16_SFLOAT,
VK_FORMAT_R16G16_SFLOAT,
VK_FORMAT_R16G16B16_SFLOAT,
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_FORMAT_R32_SFLOAT,
VK_FORMAT_R32G32_SFLOAT,
VK_FORMAT_R32G32B32_SFLOAT,
VK_FORMAT_R32G32B32A32_SFLOAT,
VK_FORMAT_B10G11R11_UFLOAT_PACK32,
VK_FORMAT_E5B9G9R9_UFLOAT_PACK32,
VK_FORMAT_D16_UNORM,
VK_FORMAT_X8_D24_UNORM_PACK32,
VK_FORMAT_D32_SFLOAT,
VK_FORMAT_S8_UINT,
VK_FORMAT_D16_UNORM_S8_UINT,
VK_FORMAT_D24_UNORM_S8_UINT,
VK_FORMAT_D32_SFLOAT_S8_UINT,
VK_FORMAT_A8_UNORM_KHR,
VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR,
};
if (isProhibitedFormat(vkFormat))
report.fatal_usage("The requested {} format is prohibited in KTX files.", toString(vkFormat));
if (!raw && !convertableFormats.count(vkFormat))
report.fatal_usage("Unsupported format for non-raw create: {}.", toString(vkFormat));
if (raw) {
if (!width)
report.fatal_usage("Option --width is missing but is required for --raw texture creation.");
if (!height)
report.fatal_usage("Option --height is missing but is required for --raw texture creation.");
}
if (width == 0u)
report.fatal_usage("The --width cannot be 0.");
if (height == 0u)
report.fatal_usage("The --height cannot be 0.");
if (layers == 0u)
report.fatal_usage("The --layers cannot be 0.");
if (levels == 0u)
report.fatal_usage("The --levels cannot be 0.");
if (depth == 0u)
report.fatal_usage("The --depth cannot be 0.");
if (raw) {
const auto maxDimension = std::max(width.value_or(1), std::max(height.value_or(1), depth.value_or(1)));
const auto maxLevels = log2(maxDimension) + 1;
if (levels.value_or(1) > maxLevels)
report.fatal_usage("Requested {} levels is too many. With base size {}x{}x{} the texture can only have {} levels at most.",
levels.value_or(1), width.value_or(1), height.value_or(1), depth.value_or(1), maxLevels);
}
if (_1d && height && height != 1u)
report.fatal_usage("For --1d textures the --height must be 1.");
if (layers && depth)
report.fatal_usage("3D array texture creation is unsupported. --layers is {} and --depth is {}.",
*layers, *depth);
if (cubemap && depth)
report.fatal_usage("Cubemaps cannot have 3D textures. --depth is {}.", *depth);
if (mipmapRuntime && levels.value_or(1) > 1u)
report.fatal_usage("Conflicting options: --runtime-mipmap cannot be used with more than 1 --levels.");
if (mipmapGenerate && mipmapRuntime)
report.fatal_usage("Conflicting options: --generate-mipmap and --runtime-mipmap cannot be used together.");
if (mipmapGenerate && raw)
report.fatal_usage("Conflicting options: --generate-mipmap cannot be used with --raw.");
if (mipmapGenerate && depth)
report.fatal_usage("Mipmap generation for 3D textures is not supported: --generate-mipmap cannot be used with --depth.");
formatDesc = createFormatDescriptor(vkFormat, report);
convertTF = parseTransferFunction(args, kConvertTf, kConvertOetf, report);
assignTF = parseTransferFunction(args, kAssignTf, kAssignOetf, report);
if (convertTF.has_value()) {
switch (convertTF.value()) {
case KHR_DF_TRANSFER_LINEAR: [[fallthrough]];
case KHR_DF_TRANSFER_SRGB:
break;
default:
report.fatal_usage("Unsupported transfer function {} for --{}.",
args[kConvertTf].as<std::string>(), kConvertTf);
}
}
convertPrimaries = parseColorPrimaries(args, kConvertPrimaries, report);
assignPrimaries = parseColorPrimaries(args, kAssignPrimaries, report);
if (convertPrimaries.has_value() && assignPrimaries == KHR_DF_PRIMARIES_UNSPECIFIED)
report.fatal_usage("Option --{} cannot be used when --{} is set to 'none'.",
kConvertPrimaries, kAssignPrimaries);
if (raw) {
if (convertTF.has_value())
report.fatal_usage("Option {} cannot be used with --{}.", kConvertTf, kRaw);
if (convertPrimaries.has_value())
report.fatal_usage("Option {} cannot be used with --{}.", kConvertPrimaries, kRaw);
if (convertTexcoordOrigin.has_value())
report.fatal_usage("Option {} cannot be used with --{}.", kConvertTexcoordOrigin, kRaw);
if (imageScale.has_value())
report.fatal_usage("Option {} cannot be used with --{}.", kScale, kRaw);
}
if (formatDesc.transfer() == KHR_DF_TRANSFER_SRGB) {
const auto error_message = "Invalid value \"{}\" to --{} for format \"{}\". Transfer function must be sRGB for sRGB formats.";
if (!convertTF.has_value() && assignTF.has_value()) {
switch (assignTF.value()) {
case KHR_DF_TRANSFER_UNSPECIFIED:
case KHR_DF_TRANSFER_SRGB:
// assign-tf must either not be specified or must be sRGB for an sRGB format
break;
default:
report.fatal_usage(error_message, args[kAssignTf].count() ? args[kAssignTf].as<std::string>() : args[kAssignOetf].as<std::string>(),
kAssignTf, args[kFormat].as<std::string>());
}
} else if (convertTF.has_value() && convertTF != KHR_DF_TRANSFER_SRGB) {
report.fatal_usage(error_message, args[kConvertTf].count() ? args[kConvertTf].as<std::string>() : args[kConvertOetf].as<std::string>(),
kConvertTf, args[kFormat].as<std::string>());
}
}
if (isFormatSRGB(vkFormat) && normalize)
report.fatal_usage("Option --{} cannot be used with sRGB formats.", kNormalize);
if (isFormatNotSRGBButHasSRGBVariant(vkFormat)) {
const auto error_message = "Invalid value \"{}\" to --{} for format \"{}\". Transfer function must not be sRGB for a non-sRGB VkFormat with sRGB variant.";
if (!convertTF.has_value() && assignTF.has_value() && assignTF == KHR_DF_TRANSFER_SRGB) {
report.fatal_usage(error_message, args[kAssignTf].count() ? args[kAssignTf].as<std::string>() : args[kAssignOetf].as<std::string>(),
kAssignTf, args[kFormat].as<std::string>());
} else if (convertTF.has_value() && convertTF == KHR_DF_TRANSFER_SRGB) {
report.fatal_usage(error_message, args[kConvertTf].count() ? args[kConvertTf].as<std::string>() : args[kConvertOetf].as<std::string>(),
kConvertTf, args[kFormat].as<std::string>());
}
}
if (args[kFailOnColorConversions].count())
failOnColorConversions = true;
if (args[kWarnOnColorConversions].count()) {
if (failOnColorConversions)
report.fatal_usage("The options --{} and --{} are mutually exclusive.",
kFailOnColorConversions, kWarnOnColorConversions);
warnOnColorConversions = true;
}
if (args[kFailOnOriginChanges].count())
failOnOriginChanges = true;
if (args[kWarnOnOriginChanges].count()) {
if (failOnOriginChanges)
report.fatal_usage("The options --{} and --{} are mutually exclusive.",
kFailOnOriginChanges, kWarnOnOriginChanges);
warnOnOriginChanges = true;
}
if(args[kPremultiplyAlpha].count()) {
if(normalize) {
report.fatal_usage("The options --{} cannot be used with --{}",
kPremultiplyAlpha, kNormalize);
}
if(raw) {
report.fatal_usage("The options --{} cannot be used with --{}",
kPremultiplyAlpha, kRaw);
}
premultiplyAlpha = true;
}
}
};
// -------------------------------------------------------------------------------------------------
/** @page ktx_create ktx create
@~English
Create a KTX2 file from various input files.
@section ktx_create_synopsis SYNOPSIS
ktx create [option...] @e input-file... @e output-file
@section ktx\_create\_description DESCRIPTION
@b ktx @b create can create, encode and supercompress a KTX2 file from the
input images specified as the @e input-file... arguments and save it as the
@e output-file. The last positional argument is treated as the @e output-file.
If the @e input-file is '-' the file will be read from the stdin.
If the @e output-path is '-' the output file will be written to the stdout.
An @e input-file prefixed with @b \@ will be read as a file name listing file.
Listing text files specify which actual files to process, one file name per line.
Names can be absolute paths or relative to the current directory when the
application is run. If the file is prefixed with @b \@\@ the names must be
relative to the listing file.
Each @e input-file must be a valid EXR (.exr), PNG (.png) or Raw (.raw) file.
PNG files with luminance (L) or luminance + alpha (LA) data will be converted
to RGB as LLL and RGBA as LLLA before processing further.
The input file formats must be compatible with the requested KTX format enum
and must have at least the same level of precision and number of channels.
Any unused channel will be discarded silently.
The number of input-files specified must match the expected number of input
images based on the used options.
@section ktx\_create\_options OPTIONS
@subsection ktx\_create\_options\_general General Options
The following are available:
<dl>
<dt>\--format &lt;enum&gt;</dt>
<dd>KTX format enum that specifies the data format for the images in the
created texture. The enum names match the VkFormat names without the
VK\_FORMAT\_ prefix. The VK\_FORMAT\_ prefix is ignored if present.
Case insensitive. Required.<br />
<br />
If the format is an ASTC format a texture object with the target
format @c R8G8B8_{SRGB,UNORM} or @c R8G8B8A8_{SRGB,UNORM} is
created then encoded to the specified ASTC format. The latter format
is chosen if alpha is present in the input. @c SRGB or @c UNORM is
chosen depending on the specified ASTC format. The ASTC-specific and
common encoder options listed @ref ktx_create_options_encoding
"below" become valid, otherwise they are ignored. This matches the
functionality of the @ref ktx_encode "ktx encode" command when an
ASTC format is specified.<br />
<br />
When used with @b \--encode it specifies the target format before
the encoding step. In this case it must be one of:
<ul>
<li>R8\_UNORM</li>
<li>R8\_SRGB</li>
<li>R8G8\_UNORM</li>
<li>R8G8\_SRGB</li>
<li>R8G8B8\_UNORM</li>
<li>R8G8B8\_SRGB</li>
<li>R8G8B8A8\_UNORM</li>
<li>R8G8B8A8\_SRGB</li>
</ul>
The format will be used to verify and load all input files into a
texture before performing any specified encoding.<br />
</dd>
<dt>\--encode basis-lz | uastc</dt>
<dd>Encode the texture with the specified codec before saving it.
This option matches the functionality of the @ref ktx_encode
"ktx encode" command. With each choice, the specific and common
encoder options listed @ref ktx_create_options_encoding "below"
become valid, otherwise they are ignored. Case-insensitive.</dd>
@snippet{doc} ktx/encode_utils_basis.h command options_basis_encoders
<dt>\--1d</dt>
<dd>Create a 1D texture. If not set the texture will be a 2D or
3D texture.</dd>
<dt>\--cubemap</dt>
<dd>Create a cubemap texture. If not set the texture will be a 2D or
3D texture.</dd>
<dt>\--raw</dt>
<dd>Create from raw image data.</dd>
<dt>\--width</dt>
<dd>Base level width in pixels. Required with @b \--raw. For non-raw, if not
set, the image width is used otherwise the image is resampled to this width
and any provided mip levels are resampled proportionately. For non-raw it
enables use of the 'Generate Mipmap' options listed under @b \--generate-mipmap
to tune the resampler.</dd>
<dt>\--height</dt>
<dd>Base level height in pixels. Required with @b \--raw. For non-raw, if not
set, the image height is used otherwise the image is resampled to this height
and any provided mip levels are resampled proportionately. For non-raw it
enables use of the 'Generate Mipmap' options listed under @b \--generate-mipmap
to tune the resampler.</dd>
<dt>\--depth</dt>
<dd>Base level depth in pixels.
If set the texture will be a 3D texture.</dd>
<dt>\--layers</dt>
<dd>Number of layers.
If set the texture will be an array texture.</dd>
<dt>\--levels</dt>
<dd>Number of mip levels. This is the number of level images to include in the texture
being created. If @b \--generate-mipmap is specified this number of level images
will be generated otherwise this number of input images must be provided.
Generates an error if the value is greater than the maximum possible for the
specified dimensions of the texture or, for non-raw, the dimensions of the base
level image as possibly modified by @b \--scale.</dd>
<dt>\--runtime-mipmap</dt>
<dd>Runtime mipmap generation mode.
Sets up the texture to request the mipmaps to be generated by the
client application at runtime.</dd>
<dt>\--generate-mipmap</dt>
<dd>Causes mipmaps to be generated during texture creation.
If @b \--levels is not specified the maximum possible mip level will
be generated. This option is mutually exclusive with
--runtime-mipmap and cannot be used with SINT, UINT or 3D textures
or @b \--raw. When set it enables the use of the following
'Generate Mipmap' options.
<dl>
<dt>\--mipmap-filter &lt;filter&gt;</dt>
<dd>Specifies the filter to use when generating the mipmaps.
Case insensitive. Ignored unless @b \--generate-mipmap, @b \--scale,
@b \--width or @b \--height are specified for non-raw input.<br />
Possible options are:
box | tent | bell | b-spline | mitchell | blackman | lanczos3 |
lanczos4 | lanczos6 | lanczos12 | kaiser | gaussian |
catmullrom | quadratic_interp | quadratic_approx |
quadratic_mix.
Defaults to lanczos4.</dd>
<dt>\--mipmap-filter-scale &lt;float&gt;</dt>
<dd>The filter scale to use.
Defaults to 1.0. Ignored unless @b \--generate-mipmap, @b \--scale,
@b \--width or @b \--height are specified for non-raw input.</dd>
<dt>\--mipmap-wrap &lt;mode&gt;</dt>
<dd>Specify how to sample pixels near the image boundaries.
Case insensitive. Ignored unless @b \--generate-mipmap, @b \--scale,
@b \--width or @b \--height are specified for non-raw input.<br />
Possible options are:
wrap | reflect | clamp.
Defaults to clamp.</dd>
</dl>
Avoid mipmap generation if the Output TF (see @ref ktx\_create\_tf\_handling
below) is non-linear and is not sRGB.
</dd>
<dt>\--scale</dt>
<dd>Scale images as they are loaded. Cannot be used with --raw.
It enables use of the 'Generate Mipmap' options listed under \--generate-mipmap
to tune the resampler.</dd>
<dt>\--normalize</dt>
<dd>Normalize input normals to have a unit length. Only valid for
linear normal textures with 2 or more components. For 2-component
inputs 2D unit normals are calculated. Do not use these 2D unit
normals to generate X+Y normals with @b \--normal-mode. For 4-component
inputs a 3D unit normal is calculated. 1.0 is used for the value of
the 4th component. Cannot be used with @b \--raw.</dd>
<dt>\--premultiply-alpha</dt>
<dd>Pre-multiplies the color components of the input pixels by the alpha component
before encoding and sets the flag in the metadata. Cannot be used with --normalize
or --raw.</dd>
<dt>\--swizzle [rgba01]{4}</dt>
<dd>KTX swizzle metadata.</dd>
<dt>\--input-swizzle [rgba01]{4}</dt>
<dd>Pre-swizzle input channels.</dd>
<dt>\--assign-tf &lt;transfer function&gt;</dt>
<dd>Force the created texture to have the specified transfer function,
ignoring the transfer function of the input file(s). Possible
options match the khr_df_transfer_e enumerators without the
KHR_DF_TRANSFER_ prefix. The KHR_DF_TRANSFER_ prefix
is ignored if present. Case nsensitive. The options are:
linear | srgb | srgb_eotf | scrgb | scrgb_eotf | itu | itu_oetf |
bt601 | bt601_oetf | bt709 | bt709_oetf | bt2020 | bt2020_oetf |
smpte170m | smpte170m_oetf | smpte170m_eotf | ntsc | ntsc_eotf |
slog | slog_oetf | slog2 | slog2_oetf | bt1886 | bt1886_eotf |
hlg_oetf | hlg_eotf | pq_oetf | pg_eotf | dcip3 | dcip3_eotf |
pal_oetf | pal625_eotf | st240 | st240_oetf | st240_eotf | acescc |
acescc_oetf | acescct | acescct_oetf | abobergb | adobergb_eotf
See @ref ktx_create_tf_handling below for important information.
</dd>
<dt>\--assign-oetf &lt;transfer function&gt;</dt>
<dd>Deprecated and will be removed. Use @b \--assign-tf instead.</dd>
<dt>\--assign-primaries &lt;primaries&gt;</dt>
<dd>Force the created texture to have the specified color primaries,
ignoring the color primaries of the input file(s). Possible options
match the khr_df_primaries_e enumerators without the
KHR_DF_PRIMARIES_ prefix. The KHR_DF_PRIMARIES_ prefix is ignored
if present. Case insensitive. The options are:
none | bt709 | srgb | bt601_ebu | bt601_smpte | bt2020 | ciexyz |
aces | acescc | ntsc1953 | pal525 | displayp3 | adobergb.
@note @c bt601-ebu and @c bt601-smpte, supported in previous
releases, have been replaced with names consistent with
khr_df_primaries_e.
</dd>
<dt>\--assign-texcoord-origin &lt;corner&gt;</dt>
<dd>Force the created texture to indicate that the texture coordinate
origin s=0, t=0 is at the specified @em corner of the logical image.
Case insensitive. Possible options are top-left | bottom-left.
-front | -back can be appended and one of these is required when
@b \--depth is specified. Must be top-left if @b \--cubemap is
specified. Absent @b —convert-texcoord-origin, the effect of this
option is to cause @e KTXorientation metadata indicating the
specified origin to be written to the output file. Example values
are "rd" (top-left) and "ru" (bottom-left) or, when @b \--depth is
specified, "rdi" (top-left-front) and "rui" (bottom-left-front).
</dd>
<dt>\--convert-tf &lt;transfer function&gt;</dt>
<dd>Convert the input image(s) to the specified transfer function, if
different from the transfer function of the input file(s). If both
this and @b \--assign-tf are specified, conversion will be
performed from the assigned transfer function to the transfer
function specified by this option, if different. Cannot be used with
@b \--raw. Case insensitive.
The options are: linear | srgb. The following srgb aliases are
also supported: srgb_eotf | scrgb | scrgb_eotf.
See @ref ktx_create_tf_handling below for more information.
</dd>
<dt>\--convert-oetf &lt;transfer function&gt;</dt>
<dd>Deprecated and will be removed. Use @b \--convert-tf instead.</dd>
<dt>\--convert-primaries &lt;primaries&gt;</dt>
<dd>Convert the input image(s) to the specified color primaries, if
different from the color primaries of the input file(s) or the one
specified by @b \--assign-primaries. If both this and
@b \--assign-primaries are specified, conversion will be performed
from the assigned primaries to the primaries specified by this
option, if different. This option is not allowed to be specified
when @b \--assign-primaries is set to 'none'.
Cannot be used with @b \--raw. Possible options match the
khr_df_primaries_e enumerators without the KHR_DF_PRIMARIES_
prefix. The KHR_DF_PRIMARIES_ prefix is ignored if present.
Case insensitive. The options are:
bt709 | srgb | bt601_ebu | bt601_smpte | bt2020 | ciexyz | aces | acescc | ntsc1953 |
pal525 | displayp3 | adobergb
@note @c bt601-ebu and @c bt601-smpte, supported in previous
releases, have been replaced with names consistent with
khr_df_primaries_e.
<dt>\--convert-texcoord-origin &lt;corner&gt;</dt>
<dd>Convert the input image(s) so the texture coordinate origin s=0,
t=0, is at the specified @em corner of the logical image. If both
this and @b \--assign-texcoord-origin are specified, conversion will
be performed from the assigned origin to the origin specified by
this option, if different. The default for images in KTX files is
top-left which corresponds to the origin in most image file
formats. Cannot be used with @b \--raw. Case insensitive.
Possible options are: top-left | bottom-left. -front | -back can be
appended and one of these is required when @b \--depth is specified.
Must be top-left if @b \--cubemap is specified.<br />
<br />
Input images whose origin does not match @em corner will be flipped
vertically. @e KTXorientation metadata indicating the
the specified origin is written to the output file. Example values
are "rd" (top-left) and "ru" (bottom-left) or, when @b \--depth is
specified, "rdi" (top-left-front) and "rui" (bottom-left-back).
Generates an error if the input image origin is unknown as is the
case with raw image data. Use @b --assign-texcoord-origin to
specify the orientation.
@note ktx create cannot rotate or flip incoming images, except
for a y-flip, so use an an image processing tool to reorient images
whose first data stream pixel is not at the logical top-left or
bottom-left of the image before using as input here. Such images
may be indicated by Exif-style orientation metadata in the file.
</dd>
<dt>\--fail-on-color-conversions</dt>
<dd>Generates an error if any input images would need to be
color converted.</dd>
<dt>\--warn-on-color-conversions</dt>
<dd>Generates a warning if any input images are color
converted. Adds warnings for explicitly requested and visually
lossless implicit conversions to that generated for visually lossy
conversions.
</dd>
<dt>\--no-warn-on-color-conversions</dt>
<dd>Disable all warnings about color conversions including that for
visually lossy conversions. Overrides
@b \--warn-on-color-conversions should both be specified.
</dd>
<dt>\--fail-on-origin-changes</dt>
<dd>Generates an error if any of the input images would need to have
their origin changed.</dd>
<dt>\--warn-on-origin-changes</dt>
<dd>Generates a warning if any of the input images have their origin
changed..</dd>
</dl>
@snippet{doc} ktx/deflate_utils.h command options_deflate
@snippet{doc} ktx/command.h command options_generic
@subsection ktx\_create\_options\_encoding Specific and Common Encoding Options
The following are available. Specific options become valid only if their
encoder has been selected. Common encoder options become valid when an
encoder they apply to has been selected. Otherwise they are ignored.
@snippet{doc} ktx/encode_utils_astc.h command options_encode_astc
@snippet{doc} ktx/encode_utils_basis.h command options_encode_basis
@snippet{doc} ktx/encode_utils_common.h command options_encode_common
@snippet{doc} ktx/metrics_utils.h command options_metrics
@section ktx_create_tf_handling TRANSFER FUNCTION HANDLING
The diagram below shows all assignments and conversions that can take place.
<!-- No way to disable Xcode syntax coloring for a file part. -->
<!-- Live with the bad Xcode rendering. -->
<!-- ASCII art created with the help of https://asciiflow.com. -->
@verbatim
┌──────────┐ ┌─────────┐
│ ├──────────────────1─────────────────►│ │
│ │ ┌───────────┐ │ │
│ Input │ │ │ │ │
│ Transfer │ │ --assign- ├──────────2──────────►│Output │
│ function │ │ tf │ ┌────────────┐ │Transfer │
│ from │ │ ├─3─►│ │ │Function │
│ file │ │ │ │ --convert- │ │ │
│ metadata │ └───────────┘ │ tf ├3,4►│ │
│ │ │ │ │ │
│ ├────────4─────────►│ │ │ │
└──────────┘ └────────────┘ └─────────┘
@endverbatim
<h4>Processing Paths</h4>
<ol>
<li>Pass through. No options specified.</li>
<li>@b \--assign-tf specified.</li>
<li>@b \--assign-tf and @b \--convert-tf specified.</li>
<li>@b \--convert-tf specified.</li>
</ol>
@subsection ktx_create_tf_handling_details Details
Transfer function handling proceeds as follows:
<ul>
<li>If @b \--format specifies one of the @c *_SRGB{,_*} formats and
Output Transfer Function is not sRGB (a.k.a scRGB) an error is generated.</li>
<li>If @b \--format does not specify one of the @c *_SRGB{,_*} formats, an
sRGB variant exists and Output Transfer Function is sRGB (a.k.a scRGB), an
error is generated.</li>
<li>Otherwise, the transfer function of the output KTX file is set to Output
Transfer Function.</li>
<li>If neither @b \--assign-tf nor @b \--convert-tf is specified:
<ul>
<li>If the Input Transfer Function is not sRGB (a.k.a scRGB) for
@c *_SRGB{,_*} formats an implicit conversion to sRGB is done, equivalent
to @b \--convert-tf srgb.</li>
<li>If the Input Transfer Function is not linear for formats that are not
one of the @c *_SRGB{,_*} formats, an implicit conversion to linear is done
equivalent to @b --convert-tf linear.</li>
</ul></li>
<li>Supported inputs for implicit or explicit conversion are linear, sRGB,
ITU (a.k.a BT601, BT.709, BT.2020 and SMPTE170M) and PQ EOTF. An error is
generated if an unsupported conversion is required.</li>
<li>Supported outputs for implicit or explicit conversion are linear and sRGB,
An error is generated if an unsupported conversion is required.</li>
<li>Output Transfer Function for a format that is not one of the @c *_SRGB{,_*}
formats can be set to a non-linear transfer function via
@b \--assign-tf.</li>
<li>A warning is generated if a visually lossy color-conversion is performed.
sRGB to linear is considered visually lossy because there is a high chance
it will introduce artifacts visible to the human eye such as banding.
The warning can be suppressed with @b \--no-warn-on-color-conversions.
A warning or an error on any color conversion can be requested with
@b \--warn-on-color-conversions or @b \--fail-on-color-conversions .
</ul>
@note When @b \--format does not specify one of the *_SRGB{,_*} formats and
Output Transfer Function is not linear:
@li the KTX file may be much less portable due to limited hardware
support of such inputs.
@li avoid using @b \--generate-mipmap as the filters can only decode
sRGB.
@li avoid encoding to ASTC, BasisLz/ETC1S or UASTC. The encoders'
quality metrics are designed for linear and sRGB.
@subsection ktx_create_tf_handling_changes Changes since last Release
<ol>
<li>@b \--assign-oetf and @b \--convert-oetf are deprecated and will be
removed. Use @b \--assign-tf and @b \--convert-tf instead.
<li>The parameter value for @b \--assign-tf can now be any of the
transfer functions known to the Khronos Data Format Specification.</li>
<li>A warning is now generated if a visually lossy color conversion will
be performed. The warning can be suppressed with
@b \--no-warn-on-color-conversions.
</li>
</ol>
@section ktx_create_exitstatus EXIT STATUS
@snippet{doc} ktx/command.h command exitstatus
@section ktx_create_history HISTORY
@par Version 4.3
- Initial version
@par Version 4.4
- Reorganize encoding options.
- Improve explanation of use of @b \--format with @b \--encode.
- Improve explanation of ASTC encoding.
@section ktx_create_author AUTHOR
- Mátyás Császár [Vader], RasterGrid www.rastergrid.com
- Daniel Rákos, RasterGrid www.rastergrid.com
- Mark Callow
*/
class CommandCreate : public Command {
private:
Combine<OptionsCreate, OptionsEncodeASTC, OptionsEncodeBasis<false>, OptionsEncodeCommon, OptionsMetrics, OptionsDeflate, OptionsMultiInSingleOut, OptionsGeneric> options;
uint32_t targetChannelCount = 0; // Derived from VkFormat
uint32_t numLevels = 0;
uint32_t numLayers = 0;
uint32_t numFaces = 0;
uint32_t baseDepth = 0;
public:
virtual int main(int argc, char* argv[]) override;
virtual void initOptions(cxxopts::Options& opts) override;
virtual void processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) override;
private:
void executeCreate();
void encodeBasis(KTXTexture2& texture, OptionsEncodeBasis<false>& opts);
void encodeASTC(KTXTexture2& texture, OptionsEncodeASTC& opts);
void compress(KTXTexture2& texture, const OptionsDeflate& opts);
private:
template <typename F>
void foreachImage(const FormatDescriptor& format, F&& func);
[[nodiscard]] KTXTexture2 createTexture(const ImageSpec& target);
void generateMipLevels(KTXTexture2& texture, std::unique_ptr<Image> image, ImageInput& inputFile,
uint32_t numMipLevels, uint32_t layerIndex, uint32_t faceIndex, uint32_t depthSliceIndex);
std::unique_ptr<Image> scaleImage(std::unique_ptr<Image> image, uint32_t width, uint32_t height);
[[nodiscard]] std::string readRawFile(const std::filesystem::path& filepath);
[[nodiscard]] std::unique_ptr<Image> loadInputImage(ImageInput& inputImageFile);
std::vector<uint8_t> convert(const std::unique_ptr<Image>& image, VkFormat format, ImageInput& inputFile);
std::unique_ptr<const ColorPrimaries> createColorPrimaries(khr_df_primaries_e primaries) const;
void selectASTCMode(uint32_t bitLength);
void determineSourceColorSpace(const ImageInput& in, SrcColorSpaceInfo& srcColorSpaceInfo);
void determineTargetColorSpace(const ImageInput& in, ImageSpec& target, ColorSpaceInfo& colorSpaceInfo);
void determineSourceOrigin(const ImageInput& in, ImageSpec::Origin& usedSourceOrigin);
void determineTargetOrigin(const ImageInput& in, ImageSpec& target, ImageSpec::Origin& usedSourceOrigin);
void checkNumInputImages();
void checkSpecsMatch(const ImageInput& current, const ImageSpec& firstSpec);
};
// -------------------------------------------------------------------------------------------------
int CommandCreate::main(int argc, char* argv[]) {
try {
parseCommandLine("ktx create",
"Create, encode and supercompress a KTX2 file from the input images specified as the\n"
" input-file... arguments and save it as the output-file.",
argc, argv);
executeCreate();
return +rc::SUCCESS;
} catch (const FatalError& error) {
return +error.returnCode;
} catch (const std::exception& e) {
fmt::print(std::cerr, "{} fatal: {}\n", commandName, e.what());
return +rc::RUNTIME_ERROR;
}
}
void CommandCreate::initOptions(cxxopts::Options& opts) {
options.init(opts);
}
void CommandCreate::checkNumInputImages() {
const auto blockSizeZ = isFormat3DBlockCompressed(options.vkFormat) ?
createFormatDescriptor(options.vkFormat, *this).basic.texelBlockDimension2 + 1u : 1u;
uint32_t expectedInputImages = 0;
for (uint32_t i = 0; i < (options.mipmapGenerate ? 1 : options.levels.value_or(1)); ++i)
// If --generate-mipmap is set the input only contains the base level images
expectedInputImages += numLayers * numFaces * ceil_div(std::max(baseDepth >> i, 1u), blockSizeZ);
if (options.inputFilepaths.size() != expectedInputImages) {
fatal_usage("Too {} input images for {} level{}, {} layer, {} face and {} depth. Provided {} but expected {}.",
options.inputFilepaths.size() > expectedInputImages ? "many" : "few",
numLevels,
options.mipmapGenerate ? " (mips generated)" : "",
numLayers,
numFaces,
baseDepth,
options.inputFilepaths.size(), expectedInputImages);
}
}
void CommandCreate::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) {
options.process(opts, args, *this);
numLevels = options.levels.value_or(1);
numLayers = options.layers.value_or(1);
numFaces = options.cubemap ? 6 : 1;
baseDepth = options.depth.value_or(1u);
if (options.raw) {
// options.levels <= max for dimensions was checked in CreateOptions::process
checkNumInputImages();
}
if (!isFormatAstc(options.vkFormat)) {
for (const char* astcOption : OptionsEncodeASTC::kAstcOptions)
if (args[astcOption].count())
fatal_usage("--{} can only be used with ASTC formats.", astcOption);
} else {
fillOptionsCodecAstc<decltype(options)>(options);
if (options.OptionsEncodeCommon::noSSE)
fatal_usage("--{} is not allowed with ASTC encode", OptionsEncodeCommon::kNoSse);
}
if (options.codec == BasisCodec::BasisLZ) {
if (options.zstd.has_value())
fatal_usage("Cannot encode to BasisLZ and supercompress with Zstd.");
if (options.zlib.has_value())
fatal_usage("Cannot encode to BasisLZ and supercompress with ZLIB.");
}
if (options.codec != BasisCodec::NONE) {
switch (options.vkFormat) {
case VK_FORMAT_R8_UNORM:
case VK_FORMAT_R8_SRGB:
case VK_FORMAT_R8G8_UNORM:
case VK_FORMAT_R8G8_SRGB:
case VK_FORMAT_R8G8B8_UNORM:
case VK_FORMAT_R8G8B8_SRGB:
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
// Allowed formats
break;
default:
fatal_usage("Only R8, RG8, RGB8, or RGBA8 UNORM and SRGB formats can be encoded, "
"but format is {}.", toString(VkFormat(options.vkFormat)));
break;
}
}
const auto basisCodec = options.codec == BasisCodec::BasisLZ || options.codec == BasisCodec::UASTC;
const auto astcCodec = isFormatAstc(options.vkFormat);
const auto canCompare = basisCodec || astcCodec;
if (basisCodec)
fillOptionsCodecBasis<decltype(options)>(options);
if (options.compare_ssim && !canCompare)
fatal_usage("--compare-ssim can only be used with BasisLZ, UASTC or ASTC encoding.");
if (options.compare_psnr && !canCompare)
fatal_usage("--compare-psnr can only be used with BasisLZ, UASTC or ASTC encoding.");
if (isFormatAstc(options.vkFormat) && !options.raw) {
options.encodeASTC = true;
switch (options.vkFormat) {
case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_4x4;
break;
case VK_FORMAT_ASTC_5x4_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_5x4;
break;
case VK_FORMAT_ASTC_5x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_5x5;
break;
case VK_FORMAT_ASTC_6x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_6x5;
break;
case VK_FORMAT_ASTC_6x6_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_6x6;
break;
case VK_FORMAT_ASTC_8x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_8x5;
break;
case VK_FORMAT_ASTC_8x6_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_8x6;
break;
case VK_FORMAT_ASTC_8x8_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_8x8;
break;
case VK_FORMAT_ASTC_10x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x5;
break;
case VK_FORMAT_ASTC_10x6_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x6;
break;
case VK_FORMAT_ASTC_10x8_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x8;
break;
case VK_FORMAT_ASTC_10x10_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x10;
break;
case VK_FORMAT_ASTC_12x10_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_12x10;
break;
case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_12x12;
break;
default:
fatal(rc::NOT_SUPPORTED, "{} is unsupported for ASTC encoding.", toString(options.vkFormat));
break;
}
}
if (options._1d && options.encodeASTC)
fatal_usage("ASTC format {} cannot be used for 1 dimensional textures (indicated by --1d).",
toString(options.vkFormat));
}
template <typename F>
void CommandCreate::foreachImage(const FormatDescriptor& format, F&& func) {
// Input file ordering is specified as the same order as the
// "levelImages" structure in the KTX 2.0 specification:
// level > layer > face > image (z_slice_of_blocks)
// aka foreach level, foreach layer, foreach face, foreach image (z_slice_of_blocks)
auto inputFileIt = options.inputFilepaths.begin();
for (uint32_t levelIndex = 0; levelIndex < (options.mipmapGenerate ? 1 : numLevels); ++levelIndex) {
const auto numDepthSlices = ceil_div(std::max(baseDepth >> levelIndex, 1u), format.basic.texelBlockDimension2 + 1u);
for (uint32_t layerIndex = 0; layerIndex < numLayers; ++layerIndex) {
for (uint32_t faceIndex = 0; faceIndex < numFaces; ++faceIndex) {
for (uint32_t depthSliceIndex = 0; depthSliceIndex < numDepthSlices; ++depthSliceIndex) {
assert(inputFileIt != options.inputFilepaths.end() && "Internal error"); // inputFilepaths size was already validated during arg parsing
func(*inputFileIt++, levelIndex, layerIndex, faceIndex, depthSliceIndex);
}
}
}
}
assert(inputFileIt == options.inputFilepaths.end() && "Internal error"); // inputFilepaths size was already validated during arg parsing
}
std::string CommandCreate::readRawFile(const std::filesystem::path& filepath) {
std::string result;
InputStream inputStream(filepath.string(), *this);
inputStream->seekg(0, std::ios::end);
if (inputStream->fail())
fatal(rc::IO_FAILURE, "Failed to seek file \"{}\": {}.", filepath.generic_string(), errnoMessage());
const auto size = inputStream->tellg();
inputStream->seekg(0);
if (inputStream->fail())
fatal(rc::IO_FAILURE, "Failed to seek file \"{}\": {}.", filepath.generic_string(), errnoMessage());
result.resize(size);
inputStream->read(result.data(), size);
if (inputStream->fail())
fatal(rc::IO_FAILURE, "Failed to read file \"{}\": {}.", filepath.generic_string(), errnoMessage());
return result;
}
void CommandCreate::executeCreate() {
const auto warningFn = [this](const std::string& w) { this->warning(w); };
KTXTexture2 texture{nullptr};
targetChannelCount = options.formatDesc.channelCount();
ImageSpec target;
ColorSpaceInfo colorSpaceInfo{};
bool firstImage = true;
ImageSpec firstImageSpec{};
uint32_t maxLevels = 1;
foreachImage(options.formatDesc, [&](
const auto& inputFilepath,
uint32_t levelIndex,
uint32_t layerIndex,
uint32_t faceIndex,
uint32_t depthSliceIndex) {
if (options.raw) {
if (std::exchange(firstImage, false)) {
target = ImageSpec{
options.width.value_or(1u),
options.height.value_or(1u),
options.depth.value_or(1u),
options.formatDesc};
if (options.cubemap && target.width() != target.height())
fatal(rc::INVALID_FILE, "--cubemap specified but the input image \"{}\" with size {}x{} is not square.",
fmtInFile(inputFilepath), target.width(), target.height());
if (options.assignTF.has_value())
target.format().setTransfer(options.assignTF.value());
if (options.assignPrimaries.has_value())
target.format().setPrimaries(options.assignPrimaries.value());
if (options.assignTexcoordOrigin.has_value())
target.setOrigin(options.assignTexcoordOrigin.value());
texture = createTexture(target);
}
const auto rawData = readRawFile(inputFilepath);
const auto expectedFileSize = ktxTexture_GetImageSize(texture, levelIndex);
if (rawData.size() != expectedFileSize)
fatal(rc::INVALID_FILE, "Raw input file \"{}\" with {} bytes for level {} does not match the expected size of {} bytes.",
fmtInFile(inputFilepath), rawData.size(), levelIndex, expectedFileSize);
const auto ret = ktxTexture_SetImageFromMemory(
texture,
levelIndex,
layerIndex,
faceIndex + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
reinterpret_cast<const ktx_uint8_t*>(rawData.data()),
rawData.size());
assert(ret == KTX_SUCCESS && "Internal error"); (void) ret;
} else {
const auto inputImageFile = ImageInput::open(inputFilepath, nullptr, warningFn);
inputImageFile->seekSubimage(0, 0); // Loading multiple subimage from the same input is not supported
ImageSpec::Origin usedSourceOrigin;
if (std::exchange(firstImage, false)) {
uint32_t targetWidth, targetHeight;
if (options.imageScale.has_value()) {
targetWidth = static_cast<uint32_t>(inputImageFile->spec().width()
* options.imageScale.value());
targetHeight = static_cast<uint32_t>(inputImageFile->spec().height()
* options.imageScale.value());
// TODO: scale depth
} else {
targetWidth = options.width.value_or(inputImageFile->spec().width());
targetHeight = options.height.value_or(inputImageFile->spec().height());
// TODO: handle resampling depth
}
target = ImageSpec{
targetWidth,
targetHeight,
options.depth.value_or(1u),
options.formatDesc};
if (options.cubemap && target.width() != target.height())
fatal(rc::INVALID_FILE, "--cubemap specified but the input image \"{}\" with size {}x{} is not square.",
fmtInFile(inputFilepath), target.width(), target.height());
if (options._1d && target.height() != 1)
fatal(rc::INVALID_FILE, "For --1d textures the input image height must be 1, but for \"{}\" it was {}.",
fmtInFile(inputFilepath), target.height());
const auto maxDimension = std::max(target.width(), std::max(target.height(), baseDepth));
maxLevels = log2(maxDimension) + 1;
if (options.levels.value_or(1) > maxLevels) {
auto errorFmt = "Requested {} levels is too many. With {} {}x{} and depth {} the texture can only have {} levels at most.";
std::string baseExpl;
if (options.width.has_value() || options.height.has_value()) {
baseExpl = "a requested base image size of";
} else if (options.imageScale.has_value()) {
baseExpl = fmt::format(
"base input image \"{}\" sized {}x{} * scale {} being",
fmtInFile(inputFilepath), inputImageFile->spec().width(),
inputImageFile->spec().height(),
options.imageScale.value());
} else {
baseExpl = fmt::format("base input image \"{}\" sized", fmtInFile(inputFilepath));
}
fatal_usage(errorFmt, options.levels.value_or(1),
baseExpl,
target.width(), target.height(),
baseDepth, maxLevels);
}
checkNumInputImages();
if (options.encodeASTC)
selectASTCMode(inputImageFile->spec().format().largestChannelBitLength());
firstImageSpec = inputImageFile->spec();
determineTargetColorSpace(*inputImageFile, target, colorSpaceInfo);
determineTargetOrigin(*inputImageFile, target, usedSourceOrigin);
texture = createTexture(target);
} else {
checkSpecsMatch(*inputImageFile, firstImageSpec);
determineSourceColorSpace(*inputImageFile, colorSpaceInfo.src);
determineSourceOrigin(*inputImageFile, usedSourceOrigin);
}
const uint32_t expectedImageWidth = std::max(firstImageSpec.width() >> levelIndex, 1u);
const uint32_t expectedImageHeight = std::max(firstImageSpec.height() >> levelIndex, 1u);
const uint32_t targetImageWidth = std::max(target.width() >> levelIndex, 1u);
const uint32_t targetImageHeight = std::max(target.height() >> levelIndex, 1u);
if (inputImageFile->spec().width() != expectedImageWidth || inputImageFile->spec().height() != expectedImageHeight) {
const auto errorFmt = "Input image \"{}\" with size {}x{} does not match expected size {}x{} for level {}.";
fatal(rc::INVALID_FILE, errorFmt, fmtInFile(inputFilepath),
inputImageFile->spec().width(),
inputImageFile->spec().height(),
// When no scaling option is specified image* == targetImage*.
expectedImageWidth, expectedImageHeight,
levelIndex);
}
auto image = loadInputImage(*inputImageFile);
// Need to do color conversion if either the transfer functions or primaries don't
// match. Primaries conversion requires decode to linear then reencode thus
// transferFunctions are always required.
if (target.format().transfer() != colorSpaceInfo.src.usedTransferFunction ||
target.format().primaries() != colorSpaceInfo.src.usedPrimaries) {
assert((target.format().primaries() == colorSpaceInfo.src.usedPrimaries
|| colorSpaceInfo.src.usedPrimaries != KHR_DF_PRIMARIES_UNSPECIFIED)
&& "determineSourceColorSpace failed to check for UNSPECIFIED.");
const auto errorFmt = "Colorspace conversion requires unsupported {} {} {}.";
if (colorSpaceInfo.src.transferFunction == nullptr) {
std::string source;
if (options.assignTF.has_value()) {
source = fmt::format("specified with --{}", options.kAssignTf);
} else {
source = fmt::format("used by input file \"{}\"", fmtInFile(inputFilepath));
}
auto errorMsg = fmt::format(errorFmt,
"decode from",
toString(colorSpaceInfo.src.usedTransferFunction),
source);
if (!options.assignTF.has_value()) {
errorMsg += fmt::format(" Use an image processing tool to convert it or use"
" --{}, with or without --{}, to specify handling.",
options.kAssignTf, options.kConvertTf);
}
fatal(rc::NOT_SUPPORTED, errorMsg);
}
if (colorSpaceInfo.dst.transferFunction == nullptr) {
// If we get here it is because (a) a transfer supported for decode but not
// encode has been set with --assign-tf and (b) a primary conversion was
// requested with --convert-primaries. CLI checks prevent an unsupported
// transfer being given to --convert-tf.
auto source = fmt::format("specified with --{}", options.convertTF.has_value()
? options.kConvertTf : options.kAssignTf);
auto errorMsg = fmt::format(errorFmt,
"encode to",
toString(target.format().transfer()),
source);
// Transfer functions derived from --format values are supported.
if (target.format().primaries() != colorSpaceInfo.src.usedPrimaries) {
errorMsg += fmt::format(" Decode and encode with transfer function is"
" required to convert primaries to {}.",
toString(target.format().primaries()));
}
fatal(rc::NOT_SUPPORTED, errorMsg);
}
if (!options.noWarnOnColorConversions) {
if (target.format().model() == KHR_DF_MODEL_RGBSDA
&& target.format().transfer() == KHR_DF_TRANSFER_LINEAR) {
uint32_t bitLength;
try {
bitLength = target.format().channelBitLength();
} catch(...) {
// This happens if channels have different bit length. Check just R.
// If format is something like RGB565, any channel length would fail
// the bitLength test so picking R doesn't matter.
bitLength = target.format().channelBitLength(KHR_DF_CHANNEL_RGBSDA_R);
}
if (bitLength < 14) {
// Per Poynton, >= 14 bits is enough to handle all transitions
// visible to a human
if (colorSpaceInfo.src.usedTransferFunction == KHR_DF_TRANSFER_SRGB
|| colorSpaceInfo.src.usedTransferFunction == KHR_DF_TRANSFER_ITU) {
warning("Input file \"{}\" is undergoing a visual lossy color conversion from {} "
"to KHR_DF_TRANSFER_LINEAR. Specify an _SRGB format with --{} to prevent "
"this warning.",
fmtInFile(inputFilepath),
toString(colorSpaceInfo.src.usedTransferFunction),
options.kFormat);
}
}
}
}
if (target.format().primaries() != colorSpaceInfo.src.usedPrimaries) {
//if (colorSpaceInfo.dst.colorPrimaries != nullptr) {
//assert(colorSpaceInfo.src.colorPrimaries != nullptr);
auto primaryTransform = colorSpaceInfo.src.colorPrimaries->transformTo(*colorSpaceInfo.dst.colorPrimaries);
if (options.failOnColorConversions)
fatal(rc::INVALID_FILE,
"Input file \"{}\" would need color conversion as input and output primaries are different. "
"Use --assign-primaries and do not use --convert-primaries to avoid unwanted color conversions.",
fmtInFile(inputFilepath));
if (options.warnOnColorConversions)
warning("Input file \"{}\" is color converted as input and output primaries are different. "
"Use --assign-primaries and do not use --convert-primaries to avoid unwanted color conversions.",
fmtInFile(inputFilepath));
// Transform transfer function with primary transform
image->transformColorSpace(*colorSpaceInfo.src.transferFunction, *colorSpaceInfo.dst.transferFunction, &primaryTransform);
} else {
if (options.failOnColorConversions)
fatal(rc::INVALID_FILE,
"Input file \"{}\" would need color conversion as input and output transfer functions are different. "
"Use --assign-tf and do not use --convert-tf to avoid unwanted color conversions.",
fmtInFile(inputFilepath));
if (options.warnOnColorConversions)
warning("Input file \"{}\" is color converted as input and output transfer functions are different. "
"Use --assign-tf and do not use --convert-tf to avoid unwanted color conversions.",
fmtInFile(inputFilepath));
// Transform transfer function without primary transform
image->transformColorSpace(*colorSpaceInfo.src.transferFunction, *colorSpaceInfo.dst.transferFunction);
}
}
// Must be set before operations like resampling. Needed even when
// transformColorSpace is not called.
image->setPrimaries(target.format().primaries());
image->setTransferFunction(target.format().transfer());
// TODO: Add auto conversion and warning? Not needed now
// because all supported source formats provide top-left images.
if (image->getWidth() != targetImageWidth || image->getHeight() != targetImageHeight)
image = scaleImage(std::move(image), targetImageWidth, targetImageHeight);
if (target.origin() != usedSourceOrigin) {
if (options.failOnOriginChanges)
fatal(rc::INVALID_FILE,
"Input file \"{}\" would need to be y-flipped as input and output origins are different. "
"Use --{} and do not use --{} to avoid unwanted origin conversions.",
fmtInFile(inputFilepath), OptionsCreate::kAssignTexcoordOrigin,
OptionsCreate::kConvertTexcoordOrigin);
if (options.warnOnOriginChanges)
warning("Input file \"{}\" is y-flipped as input and output origins are different. "
"Use --{} and do not use --{} to avoid unwanted origin conversions.",
fmtInFile(inputFilepath), OptionsCreate::kAssignTexcoordOrigin,
OptionsCreate::kConvertTexcoordOrigin);
// Only difference allowed by CLI is y down or y up.
image->yflip();
}
if (options.normalize) {
if (target.format().transfer() != KHR_DF_TRANSFER_UNSPECIFIED && target.format().transfer() != KHR_DF_TRANSFER_LINEAR) {
// Report source of problematic TF.
//
// If --format is an SRGB format
// - a fatal usage error will already have been thrown so nothing to do.
// If --format is non-SRGB format
// - absent TF options, an implicit conversion to LINEAR takes place if the file
// TF is not LINEAR or UNSPECIFIED. If it can't be converted a fatal
// unsupported conversion error will already have been thrown. Therefore
// nothing to do. But if `create` is changed to set the TF for non-SRGB
// formats from the file's TF then this error handling will need updating.
// - --assign-tf has many other possible values so that is a possible source.
// - --convert-tf can only be linear or srgb. If it's srgb and the format does
// not have an equivalent SRGB format, that is another possible source.
//const auto input_error_message = "Input file \"{}\" The transfer function to be applied to the created texture is neither linear nor none. Normalize is only available for these transfer functions.";
//const auto inputTransfer = inputImageFile->spec().format().transfer();
//bool is_file_error = (inputTransfer != KHR_DF_TRANSFER_UNSPECIFIED && inputTransfer != KHR_DF_TRANSFER_LINEAR);
const auto option_error_message = "--{} value is {}. Normalize can only be used if the transfer function is linear or none.";
if (options.convertTF.has_value()) {
fatal_usage(option_error_message, OptionsCreate::kConvertTf,
toString(options.convertTF.value()));
} else if (options.assignTF.has_value()) {
fatal_usage(option_error_message, OptionsCreate::kAssignTf,
toString(options.assignTF.value()));
}
assert(false && "target.format().transfer() is not suitable for --normalize though --assign-tf and --conver-tf were not used.");
}
image->normalize();
}
if (options.swizzleInput)
image->swizzle(*options.swizzleInput);
if (options.premultiplyAlpha) {
if(image->getComponentCount() < 4) {
const auto option_error_message = "PremultiplyAlpha can only be used if the input image has alpha channels.";
fatal_usage(option_error_message, OptionsCreate::kPremultiplyAlpha);
}
image->premultiplyAlpha();
}
const auto imageData = convert(image, options.vkFormat, *inputImageFile);
const auto ret = ktxTexture_SetImageFromMemory(
texture,
levelIndex,
layerIndex,
faceIndex + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
imageData.data(),
imageData.size());
assert(ret == KTX_SUCCESS && "Internal error"); (void) ret;
if (options.mipmapGenerate) {
uint32_t numMipLevels = options.levels.value_or(maxLevels);
generateMipLevels(texture, std::move(image), *inputImageFile, numMipLevels, layerIndex, faceIndex, depthSliceIndex);
}
}
});
// Add KTXwriter metadata
const auto writer = fmt::format("{} {}", commandName, version(options.testrun));
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
static_cast<uint32_t>(writer.size() + 1), // +1 to include the \0
writer.c_str());
// Add KTXswizzle metadata
if (options.swizzle) {
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_SWIZZLE_KEY,
static_cast<uint32_t>(options.swizzle->size() + 1), // +1 to include the \0
options.swizzle->c_str());
}
// Encode and apply compression
MetricsCalculator metrics;
metrics.saveReferenceImages(texture, options, *this);
if (options.codec != BasisCodec::NONE)
encodeBasis(texture, options);
if (options.encodeASTC)
encodeASTC(texture, options);
metrics.decodeAndCalculateMetrics(texture, options, *this);
compress(texture, options);
// Add KTXwriterScParams metadata if ASTC encoding, BasisU encoding, or other supercompression was used
const auto writerScParams = fmt::format("{}{}{}{}", options.astcOptions, options.codecOptions, options.commonOptions, options.compressOptions);
if (writerScParams.size() > 0) {
// Options always contain a leading space
assert(writerScParams[0] == ' ');
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_SCPARAMS_KEY,
static_cast<uint32_t>(writerScParams.size()),
writerScParams.c_str() + 1); // +1 to exclude leading space
}
// Save output file
const auto outputPath = std::filesystem::path(DecodeUTF8Path(options.outputFilepath));
if (outputPath.has_parent_path())
std::filesystem::create_directories(outputPath.parent_path());
OutputStream outputFile(options.outputFilepath, *this);
outputFile.writeKTX2(texture, *this);
}
// -------------------------------------------------------------------------------------------------
void CommandCreate::encodeBasis(KTXTexture2& texture, OptionsEncodeBasis<false>& opts) {
auto ret = ktxTexture2_CompressBasisEx(texture, &opts);
if (ret != KTX_SUCCESS)
fatal(rc::KTX_FAILURE, "Failed to encode KTX2 file with codec \"{}\". KTX Error: {}",
to_underlying(opts.codec), ktxErrorString(ret));
}
void CommandCreate::encodeASTC(KTXTexture2& texture, OptionsEncodeASTC& opts) {
const auto ret = ktxTexture2_CompressAstcEx(texture, &opts);
if (ret != KTX_SUCCESS)
fatal(rc::KTX_FAILURE, "Failed to encode KTX2 file with codec ASTC. KTX Error: {}", ktxErrorString(ret));
}
void CommandCreate::compress(KTXTexture2& texture, const OptionsDeflate& opts) {
if (opts.zstd) {
const auto ret = ktxTexture2_DeflateZstd(texture, *opts.zstd);
if (ret != KTX_SUCCESS)
fatal(rc::KTX_FAILURE, "Zstd deflation failed. KTX Error: {}", ktxErrorString(ret));
}
if (opts.zlib) {
const auto ret = ktxTexture2_DeflateZLIB(texture, *opts.zlib);
if (ret != KTX_SUCCESS)
fatal(rc::KTX_FAILURE, "ZLIB deflation failed. KTX Error: {}", ktxErrorString(ret));
}
}
// -------------------------------------------------------------------------------------------------
std::unique_ptr<Image> CommandCreate::loadInputImage(ImageInput& inputImageFile) {
std::unique_ptr<Image> image = nullptr;
const auto& inputFormat = inputImageFile.spec().format();
const auto width = inputImageFile.spec().width();
const auto height = inputImageFile.spec().height();
const auto inputBitLength = inputFormat.largestChannelBitLength();
const auto requestBitLength = std::max(imageio::bit_ceil(inputBitLength), 8u);
FormatDescriptor loadFormat;
switch (inputImageFile.formatType()) {
case ImageInputFormatType::exr_uint:
image = std::make_unique<rgba32image>(width, height);
loadFormat = createFormatDescriptor(VK_FORMAT_R32G32B32A32_UINT, *this);
break;
case ImageInputFormatType::exr_float:
image = std::make_unique<rgba32fimage>(width, height);
loadFormat = createFormatDescriptor(VK_FORMAT_R32G32B32A32_SFLOAT, *this);
break;
case ImageInputFormatType::npbm: [[fallthrough]];
case ImageInputFormatType::jpg: [[fallthrough]];
case ImageInputFormatType::png_l: [[fallthrough]];
case ImageInputFormatType::png_la: [[fallthrough]];
case ImageInputFormatType::png_rgb: [[fallthrough]];
case ImageInputFormatType::png_rgba:
if (requestBitLength == 8) {
image = std::make_unique<rgba8image>(width, height);
loadFormat = createFormatDescriptor(VK_FORMAT_R8G8B8A8_UNORM, *this);
break;
} else if (requestBitLength == 16) {
image = std::make_unique<rgba16image>(width, height);
loadFormat = createFormatDescriptor(VK_FORMAT_R16G16B16A16_UNORM, *this);
break;
} else {
fatal(rc::INVALID_FILE, "Unsupported format with {}-bit channels.", requestBitLength);
}
break;
}
inputImageFile.readImage(static_cast<uint8_t*>(*image), image->getByteCount(), 0, 0, loadFormat);
return image;
}
std::vector<uint8_t> convertUNORMPacked(const std::unique_ptr<Image>& image, uint32_t C0, uint32_t C1, uint32_t C2, uint32_t C3, std::string_view swizzle = "") {
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getUNORMPacked(C0, C1, C2, C3);
}
template <typename T>
std::vector<uint8_t> convertUNORM(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
using ComponentT = typename T::Color::value_type;
static constexpr auto componentCount = T::Color::getComponentCount();
static constexpr auto bytesPerComponent = sizeof(ComponentT);
static constexpr auto bits = bytesPerComponent * 8;
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getUNORM(componentCount, bits);
}
template <typename T>
std::vector<uint8_t> convertUNORMSBits(const std::unique_ptr<Image>& image, uint32_t sBits, std::string_view swizzle = "") {
using ComponentT = typename T::Color::value_type;
static constexpr auto componentCount = T::Color::getComponentCount();
static constexpr auto bytesPerComponent = sizeof(ComponentT);
static constexpr auto bits = bytesPerComponent * 8;
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getUNORM(componentCount, bits, sBits);
}
template <typename T>
std::vector<uint8_t> convertSFLOAT(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
using ComponentT = typename T::Color::value_type;
static constexpr auto componentCount = T::Color::getComponentCount();
static constexpr auto bytesPerComponent = sizeof(ComponentT);
static constexpr auto bits = bytesPerComponent * 8;
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getSFloat(componentCount, bits);
}
std::vector<uint8_t> convertB10G11R11(const std::unique_ptr<Image>& image) {
return image->getB10G11R11();
}
std::vector<uint8_t> convertE5B9G9R9(const std::unique_ptr<Image>& image) {
return image->getE5B9G9R9();
}
template <typename T>
std::vector<uint8_t> convertUINT(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
using ComponentT = typename T::Color::value_type;
static constexpr auto componentCount = T::Color::getComponentCount();
static constexpr auto bytesPerComponent = sizeof(ComponentT);
static constexpr auto bits = bytesPerComponent * 8;
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getUINT(componentCount, bits);
}
std::vector<uint8_t> convertUINTPacked(const std::unique_ptr<Image>& image,
uint32_t c0 = 0, uint32_t c1 = 0, uint32_t c2 = 0, uint32_t c3 = 0,
std::string_view swizzle = "") {
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getUINTPacked(c0, c1, c2, c3);
}
std::vector<uint8_t> convertSINTPacked(const std::unique_ptr<Image>& image,
uint32_t c0 = 0, uint32_t c1 = 0, uint32_t c2 = 0, uint32_t c3 = 0,
std::string_view swizzle = "") {
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getSINTPacked(c0, c1, c2, c3);
}
template <typename T>
std::vector<uint8_t> convertSINT(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
using ComponentT = typename T::Color::value_type;
static constexpr auto componentCount = T::Color::getComponentCount();
static constexpr auto bytesPerComponent = sizeof(ComponentT);
static constexpr auto bits = bytesPerComponent * 8;
if (!swizzle.empty())
image->swizzle(swizzle);
return image->getSINT(componentCount, bits);
}
std::vector<uint8_t> CommandCreate::convert(const std::unique_ptr<Image>& image, VkFormat vkFormat,
ImageInput& inputFile) {
const uint32_t inputBitDepth = std::max(8u, inputFile.spec().format().largestChannelBitLength());
const auto require = [&](uint32_t bitDepth) {
if (inputBitDepth < bitDepth)
fatal(rc::INVALID_FILE, "{}: Not enough precision to convert {} bit input to {} bit output for {}.",
inputFile.filename(), inputBitDepth, bitDepth, toString(vkFormat));
if (inputBitDepth > imageio::bit_ceil(bitDepth))
warning("{}: Possible loss of precision with converting {} bit input to {} bit output for {}.",
inputFile.filename(), inputBitDepth, bitDepth, toString(vkFormat));
};
const auto requireUNORM = [&](uint32_t bitDepth) {
switch (inputFile.formatType()) {
case ImageInputFormatType::png_l: [[fallthrough]];
case ImageInputFormatType::png_la: [[fallthrough]];
case ImageInputFormatType::png_rgb: [[fallthrough]];
case ImageInputFormatType::png_rgba: [[fallthrough]];
case ImageInputFormatType::npbm: [[fallthrough]];
case ImageInputFormatType::jpg:
break; // Accept
case ImageInputFormatType::exr_uint: [[fallthrough]];
case ImageInputFormatType::exr_float:
fatal(rc::INVALID_FILE, "{}: Input file data type \"{}\" does not match the expected input data type of {} bit \"{}\" for {}.",
inputFile.filename(), toString(inputFile.formatType()), bitDepth, "UNORM", toString(vkFormat));
}
require(bitDepth);
};
const auto requireSFloat = [&](uint32_t bitDepth) {
switch (inputFile.formatType()) {
case ImageInputFormatType::exr_float:
break; // Accept
case ImageInputFormatType::png_l: [[fallthrough]];
case ImageInputFormatType::png_la: [[fallthrough]];
case ImageInputFormatType::png_rgb: [[fallthrough]];
case ImageInputFormatType::png_rgba: [[fallthrough]];
case ImageInputFormatType::npbm: [[fallthrough]];
case ImageInputFormatType::jpg: [[fallthrough]];
case ImageInputFormatType::exr_uint:
fatal(rc::INVALID_FILE, "{}: Input file data type \"{}\" does not match the expected input data type of {} bit \"{}\" for {}.",
inputFile.filename(), toString(inputFile.formatType()), bitDepth, "SFLOAT", toString(vkFormat));
}
require(bitDepth);
};
const auto requireUINT = [&](uint32_t bitDepth) {
switch (inputFile.formatType()) {
case ImageInputFormatType::exr_uint:
break; // Accept
case ImageInputFormatType::png_l: [[fallthrough]];
case ImageInputFormatType::png_la: [[fallthrough]];
case ImageInputFormatType::png_rgb: [[fallthrough]];
case ImageInputFormatType::png_rgba: [[fallthrough]];
case ImageInputFormatType::npbm: [[fallthrough]];
case ImageInputFormatType::jpg: [[fallthrough]];
case ImageInputFormatType::exr_float:
fatal(rc::INVALID_FILE, "{}: Input file data type \"{}\" does not match the expected input data type of {} bit \"{}\" for {}.",
inputFile.filename(), toString(inputFile.formatType()), bitDepth, "UINT", toString(vkFormat));
}
require(bitDepth);
};
// ------------
switch (vkFormat) {
// PNG:
case VK_FORMAT_R8_UNORM: [[fallthrough]];
case VK_FORMAT_R8_SRGB:
requireUNORM(8);
return convertUNORM<r8image>(image);
case VK_FORMAT_R8G8_UNORM: [[fallthrough]];
case VK_FORMAT_R8G8_SRGB:
requireUNORM(8);
return convertUNORM<rg8image>(image);
case VK_FORMAT_R8G8B8_UNORM: [[fallthrough]];
case VK_FORMAT_R8G8B8_SRGB:
requireUNORM(8);
return convertUNORM<rgb8image>(image);
case VK_FORMAT_B8G8R8_UNORM: [[fallthrough]];
case VK_FORMAT_B8G8R8_SRGB:
requireUNORM(8);
return convertUNORM<rgb8image>(image, "bgr1");
// Verbatim copy with component reordering if needed, extra channels must be dropped.
//
// Input files that have 16-bit components must be truncated to
// 8 bits with a right-shift and a warning must be generated in the stderr.
case VK_FORMAT_R8G8B8A8_UNORM: [[fallthrough]];
case VK_FORMAT_R8G8B8A8_SRGB: [[fallthrough]];
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: [[fallthrough]];
case VK_FORMAT_A8B8G8R8_SRGB_PACK32:
requireUNORM(8);
return convertUNORM<rgba8image>(image);
case VK_FORMAT_B8G8R8A8_UNORM: [[fallthrough]];
case VK_FORMAT_B8G8R8A8_SRGB:
requireUNORM(8);
return convertUNORM<rgba8image>(image, "bgra");
// Verbatim copy with component reordering if needed, extra channels must be dropped.
// Input files that have 16-bit components must be truncated to
// 8 bits with a right-shift and a warning must be generated in the stderr.
case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x4_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x6_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x6_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x8_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x5_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x6_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x8_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x10_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x10_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
// ASTC texture data composition is performed via
// R8G8B8A8_UNORM followed by the ASTC encoding
requireUNORM(8);
assert(false && "Internal error");
return {};
// Passthrough CLI options to the ASTC encoder.
case VK_FORMAT_R4G4_UNORM_PACK8:
requireUNORM(8);
return convertUNORMPacked(image, 4, 4, 0, 0);
case VK_FORMAT_R5G6B5_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 5, 6, 5, 0);
case VK_FORMAT_B5G6R5_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 5, 6, 5, 0, "bgr1");
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 4, 4, 4, 4);
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 4, 4, 4, 4, "bgra");
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 5, 5, 5, 1);
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 5, 5, 5, 1, "bgra");
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 1, 5, 5, 5, "argb");
case VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR:
requireUNORM(8);
return convertUNORMPacked(image, 1, 5, 5, 5, "abgr");
case VK_FORMAT_A4R4G4B4_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 4, 4, 4, 4, "argb");
case VK_FORMAT_A4B4G4R4_UNORM_PACK16:
requireUNORM(8);
return convertUNORMPacked(image, 4, 4, 4, 4, "abgr");
// Input values must be rounded to the target precision.
// When the input file contains an sBIT chunk, its values must be taken into account.
case VK_FORMAT_R10X6_UNORM_PACK16:
requireUNORM(10);
return convertUNORMSBits<r16image>(image, 10);
case VK_FORMAT_R10X6G10X6_UNORM_2PACK16:
requireUNORM(10);
return convertUNORMSBits<rg16image>(image, 10);
case VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16:
requireUNORM(10);
return convertUNORMSBits<rgba16image>(image, 10);
case VK_FORMAT_R12X4_UNORM_PACK16:
requireUNORM(12);
return convertUNORMSBits<r16image>(image, 12);
case VK_FORMAT_R12X4G12X4_UNORM_2PACK16:
requireUNORM(12);
return convertUNORMSBits<rg16image>(image, 12);
case VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16:
requireUNORM(12);
return convertUNORMSBits<rgba16image>(image, 12);
// Input values must be rounded to the target precision.
// When the input file contains an sBIT chunk, its values must be taken into account.
case VK_FORMAT_R16_UNORM:
requireUNORM(16);
return convertUNORM<r16image>(image);
case VK_FORMAT_R16G16_UNORM:
requireUNORM(16);
return convertUNORM<rg16image>(image);
case VK_FORMAT_R16G16B16_UNORM:
requireUNORM(16);
return convertUNORM<rgb16image>(image);
case VK_FORMAT_R16G16B16A16_UNORM:
requireUNORM(16);
return convertUNORM<rgba16image>(image);
// Verbatim copy, extra channels must be dropped.
// Input PNG file must be 16-bit with sBIT chunk missing or signaling 16 bits.
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
requireUNORM(10);
return convertUNORMPacked(image, 2, 10, 10, 10, "argb");
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
requireUNORM(10);
return convertUNORMPacked(image, 2, 10, 10, 10, "abgr");
// Input values must be rounded to the target precision.
// When the input file contains an sBIT chunk, its values must be taken into account.
case VK_FORMAT_G8B8G8R8_422_UNORM: [[fallthrough]];
case VK_FORMAT_B8G8R8G8_422_UNORM: [[fallthrough]];
case VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16: [[fallthrough]];
case VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16: [[fallthrough]];
case VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16: [[fallthrough]];
case VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16: [[fallthrough]];
case VK_FORMAT_G16B16G16R16_422_UNORM: [[fallthrough]];
case VK_FORMAT_B16G16R16G16_422_UNORM:
fatal(rc::INVALID_ARGUMENTS, "Unsupported format for non-raw create: {}.", toString(options.vkFormat));
break;
// EXR:
case VK_FORMAT_R8_UINT:
requireSFloat(16);
return convertUINT<r8image>(image);
case VK_FORMAT_R8_SINT:
requireSFloat(16);
return convertSINT<r8image>(image);
case VK_FORMAT_R16_UINT:
requireSFloat(32);
return convertUINT<r16image>(image);
case VK_FORMAT_R16_SINT:
requireSFloat(32);
return convertSINT<r16image>(image);
case VK_FORMAT_R32_UINT:
requireUINT(32);
return convertUINT<r32image>(image);
case VK_FORMAT_R8G8_UINT:
requireSFloat(16);
return convertUINT<rg8image>(image);
case VK_FORMAT_R8G8_SINT:
requireSFloat(16);
return convertSINT<rg8image>(image);
case VK_FORMAT_R16G16_UINT:
requireSFloat(32);
return convertUINT<rg16image>(image);
case VK_FORMAT_R16G16_SINT:
requireSFloat(32);
return convertSINT<rg16image>(image);
case VK_FORMAT_R32G32_UINT:
requireUINT(32);
return convertUINT<rg32image>(image);
case VK_FORMAT_R8G8B8_UINT:
requireSFloat(16);
return convertUINT<rgb8image>(image);
case VK_FORMAT_R8G8B8_SINT:
requireSFloat(16);
return convertSINT<rgb8image>(image);
case VK_FORMAT_B8G8R8_UINT:
requireSFloat(16);
return convertUINT<rgb8image>(image, "bgr1");
case VK_FORMAT_B8G8R8_SINT:
requireSFloat(16);
return convertSINT<rgb8image>(image, "bgr1");
case VK_FORMAT_R16G16B16_UINT:
requireSFloat(32);
return convertUINT<rgb16image>(image);
case VK_FORMAT_R16G16B16_SINT:
requireSFloat(32);
return convertSINT<rgb16image>(image);
case VK_FORMAT_R32G32B32_UINT:
requireUINT(32);
return convertUINT<rgb32image>(image);
case VK_FORMAT_R8G8B8A8_UINT: [[fallthrough]];
case VK_FORMAT_A8B8G8R8_UINT_PACK32:
requireSFloat(16);
return convertUINT<rgba8image>(image);
case VK_FORMAT_R8G8B8A8_SINT: [[fallthrough]];
case VK_FORMAT_A8B8G8R8_SINT_PACK32:
requireSFloat(16);
return convertSINT<rgba8image>(image);
case VK_FORMAT_B8G8R8A8_UINT:
requireSFloat(16);
return convertUINT<rgba8image>(image, "bgra");
case VK_FORMAT_B8G8R8A8_SINT:
requireSFloat(16);
return convertSINT<rgba8image>(image, "bgra");
case VK_FORMAT_R16G16B16A16_UINT:
requireSFloat(32);
return convertUINT<rgba16image>(image);
case VK_FORMAT_R16G16B16A16_SINT:
requireSFloat(32);
return convertSINT<rgba16image>(image);
case VK_FORMAT_R32G32B32A32_UINT:
requireUINT(32);
return convertUINT<rgba32image>(image);
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
requireSFloat(16);
return convertUINTPacked(image, 2, 10, 10, 10, "argb");
case VK_FORMAT_A2R10G10B10_SINT_PACK32:
requireSFloat(16);
return convertSINTPacked(image, 2, 10, 10, 10, "argb");
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
requireSFloat(16);
return convertUINTPacked(image, 2, 10, 10, 10, "abgr");
case VK_FORMAT_A2B10G10R10_SINT_PACK32:
requireSFloat(16);
return convertSINTPacked(image, 2, 10, 10, 10, "abgr");
// The same EXR pixel types as for the decoding must be enforced.
// Extra channels must be dropped.
case VK_FORMAT_R16_SFLOAT:
requireSFloat(16);
return convertSFLOAT<r16image>(image);
case VK_FORMAT_R16G16_SFLOAT:
requireSFloat(16);
return convertSFLOAT<rg16image>(image);
case VK_FORMAT_R16G16B16_SFLOAT:
requireSFloat(16);
return convertSFLOAT<rgb16image>(image);
case VK_FORMAT_R16G16B16A16_SFLOAT:
requireSFloat(16);
return convertSFLOAT<rgba16image>(image);
case VK_FORMAT_R32_SFLOAT:
requireSFloat(32);
return convertSFLOAT<r32image>(image);
case VK_FORMAT_R32G32_SFLOAT:
requireSFloat(32);
return convertSFLOAT<rg32image>(image);
case VK_FORMAT_R32G32B32_SFLOAT:
requireSFloat(32);
return convertSFLOAT<rgb32image>(image);
case VK_FORMAT_R32G32B32A32_SFLOAT:
requireSFloat(32);
return convertSFLOAT<rgba32image>(image);
// The same EXR pixel types as for the decoding must be enforced.
// Extra channels must be dropped.
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
requireSFloat(16);
return convertB10G11R11(image);
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
requireSFloat(16);
return convertE5B9G9R9(image);
// Input data must be rounded to the target precision.
case VK_FORMAT_D16_UNORM: [[fallthrough]];
case VK_FORMAT_X8_D24_UNORM_PACK32: [[fallthrough]];
case VK_FORMAT_D32_SFLOAT: [[fallthrough]];
case VK_FORMAT_S8_UINT: [[fallthrough]];
case VK_FORMAT_D16_UNORM_S8_UINT: [[fallthrough]];
case VK_FORMAT_D24_UNORM_S8_UINT: [[fallthrough]];
case VK_FORMAT_D32_SFLOAT_S8_UINT:
fatal(rc::INVALID_ARGUMENTS, "Unsupported format for non-raw create: {}.", toString(options.vkFormat));
break;
case VK_FORMAT_A8_UNORM_KHR:
// Special case for alpha-only
requireUNORM(8);
return convertUNORM<r8image>(image, "a000");
break;
// Not supported
default:
fatal(rc::INVALID_ARGUMENTS, "Requested format conversion is not yet implemented for: {}.", toString(options.vkFormat));
}
assert(false && "Internal error");
return {};
}
KTXTexture2 CommandCreate::createTexture(const ImageSpec& target) {
ktxTextureCreateInfo createInfo;
std::memset(&createInfo, 0, sizeof(createInfo));
assert(target.depth() == baseDepth);
createInfo.vkFormat = options.vkFormat;
createInfo.numFaces = numFaces;
createInfo.numLayers = numLayers;
createInfo.isArray = options.layers > 0u;
createInfo.baseWidth = target.width();
createInfo.baseHeight = target.height();
createInfo.baseDepth = target.depth();
createInfo.numDimensions = options._1d ? 1 : (options.depth > 0u ? 3 : 2);
if (options.mipmapRuntime) {
createInfo.generateMipmaps = true;
createInfo.numLevels = 1;
} else {
createInfo.generateMipmaps = false;
if (options.mipmapGenerate) {
const auto maxDimension = std::max(target.width(), std::max(target.height(), target.depth()));
const auto maxLevels = log2(maxDimension) + 1;
createInfo.numLevels = options.levels.value_or(maxLevels);
} else {
createInfo.numLevels = options.levels.value_or(1);
}
}
KTXTexture2 texture{nullptr};
ktx_error_code_e ret = ktxTexture2_Create(&createInfo, KTX_TEXTURE_CREATE_ALLOC_STORAGE, texture.pHandle());
if (KTX_SUCCESS != ret)
fatal(rc::KTX_FAILURE, "Failed to create ktxTexture: libktx error: {}", ktxErrorString(ret));
KHR_DFDSETVAL(texture->pDfd + 1, PRIMARIES, target.format().primaries());
KHR_DFDSETVAL(texture->pDfd + 1, TRANSFER, target.format().transfer());
if(options.premultiplyAlpha) {
KHR_DFDSETVAL(texture->pDfd+1, FLAGS, KHR_DF_FLAG_ALPHA_PREMULTIPLIED);
}
// Add KTXorientation metadata
if (options.assignTexcoordOrigin.has_value() || options.convertTexcoordOrigin.has_value()) {
// This code is future-proofed by supporting "right" origin and 1d
// textures. Current imitations are enforced by CL option parsing.
std::string orientation;
orientation.resize(3);
orientation = target.origin().x == ImageSpec::Origin::eLeft ? "r" : "l";
if (!options._1d) {
orientation += target.origin().y == ImageSpec::Origin::eTop ? "d" : "u";
if (options.depth.has_value()) {
orientation += target.origin().z == ImageSpec::Origin::eFront ? "i" : "o";
}
}
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_ORIENTATION_KEY,
static_cast<uint32_t>(orientation.size() + 1), // +1 to include the \0
orientation.c_str());
}
return texture;
}
// TODO: This should probably be a method on Image.
std::unique_ptr<Image>
CommandCreate::scaleImage(std::unique_ptr<Image> image, ktx_uint32_t width, ktx_uint32_t height)
{
try {
image = image->resample(width, height,
options.mipmapFilter.value_or(options.defaultMipmapFilter).c_str(),
options.mipmapFilterScale.value_or(options.defaultMipmapFilterScale),
options.mipmapWrap.value_or(options.defaultMipmapWrap));
} catch (const std::exception& e) {
fatal(rc::RUNTIME_ERROR, "Image resampling failed: {}", e.what());
}
return image;
}
void CommandCreate::generateMipLevels(KTXTexture2& texture, std::unique_ptr<Image> image, ImageInput& inputFile,
uint32_t numMipLevels, uint32_t layerIndex, uint32_t faceIndex, uint32_t depthSliceIndex) {
if (isFormatINT(static_cast<VkFormat>(texture->vkFormat)))
fatal(rc::NOT_SUPPORTED, "Mipmap generation for SINT or UINT format {} is not supported.",
toString(static_cast<VkFormat>(texture->vkFormat)));
const auto baseWidth = image->getWidth();
const auto baseHeight = image->getHeight();
for (uint32_t mipLevelIndex = 1; mipLevelIndex < numMipLevels; ++mipLevelIndex) {
const auto mipImageWidth = std::max(1u, baseWidth >> (mipLevelIndex));
const auto mipImageHeight = std::max(1u, baseHeight >> (mipLevelIndex));
try {
image = image->resample(mipImageWidth, mipImageHeight,
options.mipmapFilter.value_or(options.defaultMipmapFilter).c_str(),
options.mipmapFilterScale.value_or(options.defaultMipmapFilterScale),
options.mipmapWrap.value_or(options.defaultMipmapWrap));
} catch (const std::exception& e) {
fatal(rc::RUNTIME_ERROR, "Mipmap generation failed: {}", e.what());
}
if (options.normalize)
image->normalize();
const auto imageData = convert(image, options.vkFormat, inputFile);
const auto ret = ktxTexture_SetImageFromMemory(
texture,
mipLevelIndex,
layerIndex,
faceIndex + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
imageData.data(),
imageData.size());
assert(ret == KTX_SUCCESS && "Internal error"); (void) ret;
}
}
void CommandCreate::selectASTCMode(uint32_t bitLength) {
if (options.mode == KTX_PACK_ASTC_ENCODER_MODE_DEFAULT) {
// If no astc mode option is specified and if input is <= 8bit
// default to LDR otherwise default to HDR
options.mode = bitLength <= 8 ? KTX_PACK_ASTC_ENCODER_MODE_LDR : KTX_PACK_ASTC_ENCODER_MODE_HDR;
} else {
if (bitLength > 8 && options.mode == KTX_PACK_ASTC_ENCODER_MODE_LDR)
// Input is > 8-bit and user wants LDR, issue quality loss warning.
warning("Input file is 16-bit but ASTC LDR option is specified. Expect quality loss in the output.");
else if (bitLength < 16 && options.mode == KTX_PACK_ASTC_ENCODER_MODE_HDR)
// Input is < 16-bit and user wants HDR, issue warning.
warning("Input file is not 16-bit but HDR option is specified.");
}
// ASTC Encoding is performed by first creating a RGBA8 texture then encode it afterward
// Encode based on non-8-bit input (aka true HDR) is currently not supported by
// ktxTexture2_CompressAstcEx. Once supported suitable formats can be chosen here
if (isFormatSRGB(options.vkFormat))
options.vkFormat = VK_FORMAT_R8G8B8A8_SRGB;
else
options.vkFormat = VK_FORMAT_R8G8B8A8_UNORM;
}
std::unique_ptr<const ColorPrimaries> CommandCreate::createColorPrimaries(khr_df_primaries_e primaries) const {
switch (primaries) {
case KHR_DF_PRIMARIES_BT709:
return std::make_unique<ColorPrimariesBT709>();
case KHR_DF_PRIMARIES_BT601_EBU:
return std::make_unique<ColorPrimariesBT601_625_EBU>();
case KHR_DF_PRIMARIES_BT601_SMPTE:
return std::make_unique<ColorPrimariesBT601_525_SMPTE>();
case KHR_DF_PRIMARIES_BT2020:
return std::make_unique<ColorPrimariesBT2020>();
case KHR_DF_PRIMARIES_CIEXYZ:
return std::make_unique<ColorPrimariesCIEXYZ>();
case KHR_DF_PRIMARIES_ACES:
return std::make_unique<ColorPrimariesACES>();
case KHR_DF_PRIMARIES_ACESCC:
return std::make_unique<ColorPrimariesACEScc>();
case KHR_DF_PRIMARIES_NTSC1953:
return std::make_unique<ColorPrimariesNTSC1953>();
case KHR_DF_PRIMARIES_PAL525:
return std::make_unique<ColorPrimariesPAL525>();
case KHR_DF_PRIMARIES_DISPLAYP3:
return std::make_unique<ColorPrimariesDisplayP3>();
case KHR_DF_PRIMARIES_ADOBERGB:
return std::make_unique<ColorPrimariesAdobeRGB>();
default:
assert(false);
// We return BT709 by default if some error happened
return std::make_unique<ColorPrimariesBT709>();
}
}
void CommandCreate::determineSourceColorSpace(const ImageInput& in, SrcColorSpaceInfo& srcColorSpaceInfo) {
const ImageSpec& spec = in.spec();
// Primaries handling:
//
// 1. Use assign-primaries option value, if set.
// 2. Use primaries info given by plugin.
// 3. If no primaries info and input is PNG use PNG spec.
// recommendation of BT709/sRGB otherwise leave as
// UNSPECIFIED.
// 4. If convert-primaries is specified but no primaries info is
// given by the plugin then fail.
srcColorSpaceInfo.colorPrimaries = nullptr;
srcColorSpaceInfo.usedPrimaries = spec.format().primaries();
if (options.assignPrimaries.has_value()) {
srcColorSpaceInfo.usedPrimaries = options.assignPrimaries.value();
} else if (spec.format().primaries() == KHR_DF_PRIMARIES_UNSPECIFIED) {
if (!in.formatName().compare("png")) {
warning("No color primaries in PNG input file \"{}\", defaulting to BT.709.", in.filename());
srcColorSpaceInfo.usedPrimaries = KHR_DF_PRIMARIES_BT709;
} // else
// Leave as unspecified.
}
if (options.convertPrimaries.has_value()) {
if (srcColorSpaceInfo.usedPrimaries == KHR_DF_PRIMARIES_UNSPECIFIED) {
fatal(rc::INVALID_FILE, "Cannot convert primaries as no information about the color primaries "
"is available in the input file \"{}\". Use --{} to specify one.", in.filename(),
options.kAssignPrimaries);
} else if (options.convertPrimaries.value() != srcColorSpaceInfo.usedPrimaries) {
srcColorSpaceInfo.colorPrimaries = createColorPrimaries(srcColorSpaceInfo.usedPrimaries);
}
}
// Transfer function handling in priority order:
//
// 1. Use assign-tf option value, if set.
// 2. Use transfer function signalled by plugin as the input transfer
// function if linear, sRGB, ITU, or PQ EOTF. For all others, throw
// error.
// 3. If ICC profile signalled, throw error. Known ICC profiles are
// handled by the plugin.
// 4. If gamma of 1.0 signalled assume linear input transfer function.
// If gamma of .45454 signalled, set up for conversion from gamma
// and warn user about the conversion.
// If gamma of 0.0 is signalled, for PNG follow W3C recommendation
// per step 5. For any other gamma value, just convert it.
// 5. If no color info is signalled, and input is PNG follow W3C
// recommendation of sRGB for 8-bit, linear otherwise. For other input
// formats throw error.
// 6. Convert transfer function based on convert-tf option value or as
// described above.
srcColorSpaceInfo.transferFunction = nullptr;
if (options.assignTF.has_value()) {
srcColorSpaceInfo.usedTransferFunction = options.assignTF.value();
} else {
// Set image's transfer function as indicated by metadata.
srcColorSpaceInfo.usedTransferFunction = spec.format().transfer();
if (spec.format().transfer() == KHR_DF_TRANSFER_UNSPECIFIED) {
if (spec.format().iccProfileName().size()) {
fatal(rc::INVALID_FILE,
"Input file \"{}\" contains unsupported ICC profile \"{}\". Use --{} to specify a different one.",
in.filename(), spec.format().iccProfileName(),
options.kAssignTf);
} else if (spec.format().oeGamma() > 0.0f) {
if (spec.format().oeGamma() > .45450f && spec.format().oeGamma() < .45460f) {
// N.B The previous loader matched oeGamma .45455 to the sRGB
// transfer function and did not do a transformation. In this
// loader we decode and reencode. Previous behavior can be
// obtained with the --assign-tf option.
//
// This change results in 1 bit differences in the LSB of
// some color values noticeable only when directly comparing
// images produced before and after this change of loader.
warning("Converting gamma 2.2f to sRGB. Use --{} srgb to force treating input as sRGB.",
options.kAssignTf);
srcColorSpaceInfo.transferFunction = std::make_unique<TransferFunctionGamma>(spec.format().oeGamma());
} else if (spec.format().oeGamma() == 1.0) {
srcColorSpaceInfo.usedTransferFunction = KHR_DF_TRANSFER_LINEAR;
} else if (spec.format().oeGamma() > 0.0f) {
// We allow any gamma, there is no reason why we could not
// allow such input
srcColorSpaceInfo.transferFunction = std::make_unique<TransferFunctionGamma>(spec.format().oeGamma());
} else if (spec.format().oeGamma() == 0.0f) {
if (!in.formatName().compare("png")) {
// If 8-bit, treat as sRGB, otherwise treat as linear.
if (spec.format().channelBitLength() == 8) {
srcColorSpaceInfo.usedTransferFunction = KHR_DF_TRANSFER_SRGB;
} else {
srcColorSpaceInfo.usedTransferFunction = KHR_DF_TRANSFER_LINEAR;
}
warning("Ignoring reported gamma of 0.0f in {}-bit PNG input file \"{}\". Handling as {}.",
spec.format().channelBitLength(), in.filename(), toString(srcColorSpaceInfo.usedTransferFunction));
} else {
fatal(rc::INVALID_FILE,
"Input file \"{}\" has gamma 0.0f. Use --{} to specify transfer function.",
options.kAssignTf);
}
} else {
if (!options.convertTF.has_value()) {
fatal(rc::INVALID_FILE, "Gamma {} not automatically supported by KTX. Specify handing with "
"--{} or --{}.", spec.format().oeGamma(),
options.kConvertTf, options.kAssignTf);
}
}
} else if (!in.formatName().compare("png")) {
// If 8-bit, treat as sRGB, otherwise treat as linear.
if (spec.format().channelBitLength() == 8) {
srcColorSpaceInfo.usedTransferFunction = KHR_DF_TRANSFER_SRGB;
} else {
srcColorSpaceInfo.usedTransferFunction = KHR_DF_TRANSFER_LINEAR;
}
warning("No transfer function can be determined from {}-bit PNG input file \"{}\", defaulting to {}. Use --{} to override.",
spec.format().channelBitLength(), in.filename(),
toString(srcColorSpaceInfo.usedTransferFunction),
options.kAssignTf);
}
}
}
if (srcColorSpaceInfo.transferFunction == nullptr) {
switch (srcColorSpaceInfo.usedTransferFunction) {
case KHR_DF_TRANSFER_LINEAR:
srcColorSpaceInfo.transferFunction = std::make_unique<TransferFunctionLinear>();
break;
case KHR_DF_TRANSFER_SRGB:
srcColorSpaceInfo.transferFunction = std::make_unique<TransferFunctionSRGB>();
break;
case KHR_DF_TRANSFER_ITU:
srcColorSpaceInfo.transferFunction = std::make_unique<TransferFunctionITU>();
break;
case KHR_DF_TRANSFER_PQ_EOTF:
srcColorSpaceInfo.transferFunction = std::make_unique<TransferFunctionBT2100_PQ_EOTF>();
break;
default:
// Lack of will be handled if a color transformation attempted.
break;
}
}
}
void CommandCreate::determineTargetColorSpace(const ImageInput& in, ImageSpec& target, ColorSpaceInfo& colorSpaceInfo) {
// Primaries handling:
//
// 1. Use assign-primaries option value, if set.
// 2. Use primaries info given by plugin.
// 3. If no primaries info and input is PNG use PNG spec.
// recommendation of BT709/sRGB otherwise leave as
// UNSPECIFIED.
// 4. If convert-primaries is specified but no primaries info is
// given by the plugin then fail.
// 5. If convert-primaries is specified and primaries info determined
// above is different then set up conversion.
determineSourceColorSpace(in, colorSpaceInfo.src);
// Set Primaries
// If convert-primaries is specified and primaries info determined
// above is different then set up conversion.
if (options.convertPrimaries.has_value()) {
assert(colorSpaceInfo.src.usedPrimaries != KHR_DF_PRIMARIES_UNSPECIFIED
&& "determineSourceColorSpace failed to check for UNSPECIFIED.");
target.format().setPrimaries(options.convertPrimaries.value());
// Okay to set this even if no conversion needed.
colorSpaceInfo.dst.colorPrimaries = createColorPrimaries(target.format().primaries());
} else {
target.format().setPrimaries(colorSpaceInfo.src.usedPrimaries);
}
// target transfer is set from the --format value or --assignTF or --convertTF.
if (options.assignTF.has_value())
target.format().setTransfer(options.assignTF.value());
if (options.convertTF.has_value())
target.format().setTransfer(options.convertTF.value());
switch (target.format().transfer()) {
case KHR_DF_TRANSFER_LINEAR:
colorSpaceInfo.dst.transferFunction = std::make_unique<TransferFunctionLinear>();
break;
case KHR_DF_TRANSFER_SRGB:
colorSpaceInfo.dst.transferFunction = std::make_unique<TransferFunctionSRGB>();
break;
default:
// Lack of will be handled if color transformation attempted.
colorSpaceInfo.dst.transferFunction = nullptr;
}
}
void CommandCreate::determineSourceOrigin(const ImageInput& in, ImageSpec::Origin& usedSourceOrigin) {
if (options.assignTexcoordOrigin.has_value()) {
usedSourceOrigin = options.assignTexcoordOrigin.value();
} else {
usedSourceOrigin = in.spec().origin();
}
}
void CommandCreate::determineTargetOrigin(const ImageInput& in, ImageSpec& target,
ImageSpec::Origin& usedSourceOrigin) {
determineSourceOrigin(in, usedSourceOrigin);
target.setOrigin(usedSourceOrigin);
if (options.convertTexcoordOrigin.has_value()) {
if (usedSourceOrigin.unspecified()) {
fatal(rc::INVALID_FILE, "Cannot convert texcoord origin as no information about the origin "
"is available in the input file \"{}\". Use --{} to specify one.",
in.filename(), OptionsCreate::kAssignTexcoordOrigin);
} else if (options.convertTexcoordOrigin.value() != usedSourceOrigin) {
target.setOrigin(options.convertTexcoordOrigin.value());
}
}
}
void CommandCreate::checkSpecsMatch(const ImageInput& currentFile, const ImageSpec& firstSpec) {
const FormatDescriptor& firstFormat = firstSpec.format();
const FormatDescriptor& currentFormat = currentFile.spec().format();
if (currentFormat.transfer() != firstFormat.transfer()) {
if (options.assignTF.has_value()) {
warning("Input image \"{}\" has different transfer function ({}) than the first image ({})"
" but will be treated identically as specified by the --assign-tf option.",
currentFile.filename(), toString(currentFormat.transfer()), toString(firstFormat.transfer()));
} else if (options.convertTF.has_value()) {
warning("Input image \"{}\" has different transfer function ({}) than the first image ({})"
" and thus will go through different transfer function conversion to the target transfer"
" function specified by the --convert-tf option.",
currentFile.filename(), toString(currentFormat.transfer()), toString(firstFormat.transfer()));
} else {
fatal(rc::INVALID_FILE, "Input image \"{}\" has different transfer function ({}) than the first image ({})."
" Use --assign-tf or --convert-tf to specify handling and stop this error.",
currentFile.filename(), toString(currentFormat.transfer()), toString(firstFormat.transfer()));
}
}
if (currentFormat.oeGamma() != firstFormat.oeGamma()) {
auto currentGamma = currentFormat.oeGamma() != -1 ? std::to_string(currentFormat.oeGamma()) : "no gamma";
auto firstGamma = firstFormat.oeGamma() != -1 ? std::to_string(firstFormat.oeGamma()) : "no gamma";
if (options.assignTF.has_value()) {
warning("Input image \"{}\" has different gamma ({}) than the first image ({})"
" but will be treated identically as specified by the --assign-tf option.",
currentFile.filename(), currentGamma, firstGamma);
} else if (options.convertTF.has_value()) {
warning("Input image \"{}\" has different gamma ({}) than the first image ({})"
" and thus will go through different transfer function conversion to the target transfer"
" function specified by the --convert-tf option.",
currentFile.filename(), currentGamma, firstGamma);
} else {
fatal(rc::INVALID_FILE, "Input image \"{}\" has different gamma ({}) than the first image ({})."
" Use --assign-tf or --convert-tf to specify handling and stop this error.",
currentFile.filename(), currentGamma, firstGamma);
}
}
if (currentFormat.primaries() != firstFormat.primaries()) {
if (options.assignPrimaries.has_value()) {
warning("Input image \"{}\" has different primaries ({}) than the first image ({})"
" but will be treated identically as specified by the --assign-primaries option.",
currentFile.filename(), toString(currentFormat.primaries()), toString(firstFormat.primaries()));
} else if (options.convertPrimaries.has_value()) {
warning("Input image \"{}\" has different primaries ({}) than the first image ({})"
" and thus will go through different primaries conversion to the target primaries"
" specified by the --convert-primaries option.",
currentFile.filename(), toString(currentFormat.primaries()), toString(firstFormat.primaries()));
} else {
fatal(rc::INVALID_FILE, "Input image \"{}\" has different primaries ({}) than the first image ({})."
" Use --assign-primaries or --convert-primaries to specify handling and stop this error.",
currentFile.filename(), toString(currentFormat.primaries()), toString(firstFormat.primaries()));
}
}
if (currentFormat.channelCount() != firstFormat.channelCount()) {
warning("Input image \"{}\" has a different component count than the first image.", currentFile.filename());
}
if (currentFile.spec().origin() != firstSpec.origin()) {
if (options.assignTexcoordOrigin.has_value()) {
warning("Input image \"{}\" has different texcoord origin ({}) than the first image ({})"
" but will be treated identically as specified by the --assign-texcoord-origin option.",
currentFile.filename(), toString(currentFile.spec().origin()), toString(firstSpec.origin()));
} else if (options.convertTexcoordOrigin.has_value()) {
warning("Input image \"{}\" has different texcoord origin ({}) than the first image ({})"
" and thus will go through different origin conversion to the target origin"
" specified by the --convert-texcoord-origin option.",
currentFile.filename(), toString(currentFile.spec().origin()), toString(firstSpec.origin()));
} else {
fatal(rc::INVALID_FILE, "Input image \"{}\" has different texcoord origin ({}) than the first image ({})."
" Use --assign-texcoord-origin or --convert-texcoord-origin to specify handling and stop this error.",
currentFile.filename(), toString(currentFile.spec().origin()), toString(firstSpec.origin()));
}
}
}
} // namespace ktx
KTX_COMMAND_ENTRY_POINT(ktxCreate, ktx::CommandCreate)