321 lines
12 KiB
C++
321 lines
12 KiB
C++
// Copyright 2022-2023 The Khronos Group Inc.
|
|
// Copyright 2022-2023 RasterGrid Kft.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#include "command.h"
|
|
#include "platform_utils.h"
|
|
#include "deflate_utils.h"
|
|
#include "formats.h"
|
|
#include "sbufstream.h"
|
|
#include "utility.h"
|
|
#include "validate.h"
|
|
#include "ktx.h"
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <regex>
|
|
#include <unordered_map>
|
|
|
|
#include <cxxopts.hpp>
|
|
#include <fmt/ostream.h>
|
|
#include <fmt/printf.h>
|
|
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
namespace ktx {
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
/** @page ktx_deflate ktx deflate
|
|
@~English
|
|
|
|
Deflate (supercompress) a KTX2 file.
|
|
|
|
@section ktx_deflate_synopsis SYNOPSIS
|
|
ktx deflate [option...] @e input-file @e output-file
|
|
|
|
@section ktx_deflate_description DESCRIPTION
|
|
@b ktx @b deflate deflates (supercompresses) the KTX file specified as the
|
|
@e input-file and saves it 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.
|
|
If the input file is already supercompressed it will be inflated then
|
|
supercompressed again using the options specified here and a warning will
|
|
be issued. If the input file is invalid the first encountered validation
|
|
error is displayed to the stderr and the command exits with the relevant
|
|
non-zero status code.
|
|
|
|
@b ktx @b deflate cannot be applied to KTX files that have been
|
|
supercompressed with BasisLZ.
|
|
|
|
@section ktx\_deflate\_options OPTIONS
|
|
The following options are available:
|
|
@snippet{doc} ktx/deflate_utils.h command options_deflate
|
|
<dl>
|
|
<dt>-q, --quiet</dt>
|
|
<dd>Silence warning about already supercompressed input fiile.</dd>
|
|
<dt>-e, --warnings-as-errors</dt>
|
|
<dd>Treat warnings as errors.</dd>
|
|
</dl>
|
|
@snippet{doc} ktx/command.h command options_generic
|
|
|
|
@section ktx_deflate_exitstatus EXIT STATUS
|
|
@snippet{doc} ktx/command.h command exitstatus
|
|
|
|
@section ktx_deflate_history HISTORY
|
|
|
|
@par Version 4.0
|
|
- Initial version
|
|
|
|
@section ktx_deflate_author AUTHOR
|
|
- Mark Callow [\@MarkCallow]
|
|
*/
|
|
class CommandDeflate : public Command {
|
|
enum {
|
|
all = -1,
|
|
};
|
|
|
|
struct Options {
|
|
inline static const char* kQuiet = "quiet";
|
|
inline static const char* kWarningsAsErrors = "warnings-as-errors";
|
|
bool quiet = false;
|
|
bool warningsAsErrors = false;
|
|
void init(cxxopts::Options& opts);
|
|
void process(cxxopts::Options& opts, cxxopts::ParseResult& args, Reporter& report);
|
|
};
|
|
|
|
Combine<Options, OptionsDeflate, OptionsSingleInSingleOut, OptionsGeneric> options;
|
|
|
|
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 executeDeflate();
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
int CommandDeflate::main(int argc, char* argv[]) {
|
|
try {
|
|
parseCommandLine("ktx deflate",
|
|
"Deflate (supercompress) the KTX file specified as the input-file\n"
|
|
" and save it as the output-file.",
|
|
argc, argv);
|
|
executeDeflate();
|
|
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 CommandDeflate::Options::init(cxxopts::Options& opts) {
|
|
opts.add_options()
|
|
(kQuiet, "Don't print warning when input file is already supercompressed.")
|
|
(kWarningsAsErrors, "Exit with error when input file is already supercompressed");
|
|
}
|
|
|
|
void CommandDeflate::Options::process(cxxopts::Options&,
|
|
cxxopts::ParseResult& args,
|
|
Reporter& report) {
|
|
quiet = args[kQuiet].as<bool>();
|
|
warningsAsErrors = args[kWarningsAsErrors].as<bool>();
|
|
if (quiet && warningsAsErrors) {
|
|
report.fatal_usage("Cannot specify both --{} and --{}.",
|
|
this->kQuiet, this->kWarningsAsErrors);
|
|
}
|
|
}
|
|
|
|
void CommandDeflate::initOptions(cxxopts::Options& opts) {
|
|
options.init(opts);
|
|
}
|
|
|
|
void CommandDeflate::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) {
|
|
options.process(opts, args, *this);
|
|
if (!options.zstd && !options.zlib) {
|
|
fatal_usage("Must specify --{} or --{}.",
|
|
OptionsDeflate::kZStd, OptionsDeflate::kZLib);
|
|
}
|
|
}
|
|
|
|
void CommandDeflate::executeDeflate() {
|
|
InputStream inputStream(options.inputFilepath, *this);
|
|
validateToolInput(inputStream, fmtInFile(options.inputFilepath), *this);
|
|
|
|
KTXTexture2 texture{nullptr};
|
|
StreambufStream<std::streambuf*> ktx2Stream{inputStream->rdbuf(), std::ios::in | std::ios::binary};
|
|
auto ret = ktxTexture2_CreateFromStream(ktx2Stream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, texture.pHandle());
|
|
if (ret != KTX_SUCCESS)
|
|
fatal(rc::INVALID_FILE, "Failed to create KTX2 texture: {}", ktxErrorString(ret));
|
|
|
|
if (texture->supercompressionScheme != KTX_SS_NONE) {
|
|
switch (texture->supercompressionScheme) {
|
|
case KTX_SS_ZLIB:
|
|
case KTX_SS_ZSTD:
|
|
if (!options.quiet) {
|
|
warning("Modifying existing {} supercompression of {}.",
|
|
toString(texture->supercompressionScheme),
|
|
options.inputFilepath);
|
|
}
|
|
break;
|
|
default:
|
|
fatal(rc::INVALID_FILE,
|
|
"Cannot further deflate a KTX2 file supercompressed with {}.",
|
|
toString(texture->supercompressionScheme));
|
|
}
|
|
}
|
|
|
|
if (options.zstd) {
|
|
ret = ktxTexture2_DeflateZstd(texture, *options.zstd);
|
|
if (ret != KTX_SUCCESS)
|
|
fatal(rc::IO_FAILURE, "Zstd deflation failed. KTX Error: {}", ktxErrorString(ret));
|
|
}
|
|
|
|
if (options.zlib) {
|
|
ret = ktxTexture2_DeflateZLIB(texture, *options.zlib);
|
|
if (ret != KTX_SUCCESS)
|
|
fatal(rc::IO_FAILURE, "ZLIB deflation failed. KTX Error: {}", ktxErrorString(ret));
|
|
}
|
|
|
|
const auto& findMetadataValue = [&](const char* const key) {
|
|
const char* value;
|
|
uint32_t valueLen;
|
|
std::string result;
|
|
auto ret = ktxHashList_FindValue(&texture->kvDataHead, key,
|
|
&valueLen, (void**)&value);
|
|
if (ret == KTX_SUCCESS) {
|
|
// The values we are looking for are required to be NUL terminated.
|
|
result.assign(value, valueLen - 1);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const auto updateMetadataValue = [&](const char* const key,
|
|
const std::string& value) {
|
|
ktxHashList_DeleteKVPair(&texture->kvDataHead, key);
|
|
ktxHashList_AddKVPair(&texture->kvDataHead, key,
|
|
static_cast<uint32_t>(value.size() + 1), // +1 to include \0
|
|
value.c_str());
|
|
};
|
|
|
|
// ======= KTXwriter and KTXwriterScParams metadata handling =======
|
|
//
|
|
// In order to preserve encoding parameters applied to the data with
|
|
// other apps prior to this deflate operation, `deflate` does the
|
|
// following if KTXwriterScParams data exists in the input file:
|
|
//
|
|
// 1. If the writer was one of the ktx suite (i.e. create or encode)
|
|
// and KTXwriterScParams contains non-deflate options, use the
|
|
// original KTXwriter. Replace an existing deflate option with
|
|
// that currently specified or append it as new.
|
|
//
|
|
// The original writer will obviously understand its own
|
|
// non-deflate options and, since it is part of the ktx suite
|
|
// it will understand the updated or new deflate option that will
|
|
// be added.
|
|
//
|
|
// Cheeky! Spec. for KTXwriter says "only the most recent writer
|
|
// Should be identified." For KTXwriterScParams it says the writer
|
|
// should "append the (new) options" when "building on operations
|
|
// done previously." To somewhat resolve the conflict it changes
|
|
// the previous "only" to "in general."
|
|
//
|
|
// 2. If the writer was another tool, preserve its options in
|
|
// KTWwriterScParams labelled with its name and append the
|
|
// currently specified deflate option like so
|
|
//
|
|
// --zstd 18 | (from <name>) option1 option2 ...
|
|
//
|
|
// where <name> is the first word of the original KTXwriter metadata,
|
|
// e.g, "tokt". Rewrite KTXwriter with the name of this tool.
|
|
//
|
|
// 3. If the writer was ktxsc or toktx remove any original deflate
|
|
// option from the preserved parameters as we know those option
|
|
// names.
|
|
|
|
bool changeWriter = true;
|
|
std::string writerScParams;
|
|
std::string origWriterName;
|
|
writerScParams = findMetadataValue(KTX_WRITER_SCPARAMS_KEY);
|
|
if (!writerScParams.empty()) {
|
|
std::string writer = findMetadataValue(KTX_WRITER_KEY);
|
|
if (!writer.empty()) {
|
|
std::regex e("ktx (?:create|deflate|encode|transcode)");
|
|
std::smatch deflateOptionMatch;
|
|
if (std::regex_search(writer, e)) {
|
|
// Writer is member of the ktx suite.
|
|
// Look for existing deflate option
|
|
e = " ?--(?:zlib|zstd) [1-9][0-9]?";
|
|
(void)std::regex_search(writerScParams, deflateOptionMatch, e);
|
|
} else {
|
|
// Writer is not a member of the ktx suite
|
|
e = "ktxsc|toktx";
|
|
if (std::regex_search(writer, e)) {
|
|
// Look for toktx/ktxsc deflate option
|
|
e = " ?--zcmp ?[1-9]?[0-9]?";
|
|
(void)std::regex_search(writerScParams,
|
|
deflateOptionMatch, e);
|
|
}
|
|
origWriterName = writer.substr(0, writer.find_first_of(' '));
|
|
}
|
|
// Remove existing deflate option since its value will not apply
|
|
// to the newly deflated data.
|
|
for (uint32_t i = 0; i < deflateOptionMatch.size(); i++) {
|
|
writerScParams.replace(deflateOptionMatch.position(i),
|
|
deflateOptionMatch.length(i),
|
|
"");
|
|
}
|
|
// Does ScParams still have data and is the original writer a
|
|
// member of the ktx suite?
|
|
if (!writerScParams.empty() && origWriterName.empty()) {
|
|
changeWriter = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changeWriter) {
|
|
// Create or modify KTXwriter metadata.
|
|
const auto writer = fmt::format("{} {}", commandName, version(options.testrun));
|
|
updateMetadataValue(KTX_WRITER_KEY, writer);
|
|
}
|
|
|
|
// Format new writerScParams.
|
|
auto newScParams = fmt::format("{}", options.compressOptions);
|
|
// Options always contain a leading space
|
|
assert(newScParams[0] == ' ');
|
|
if (!writerScParams.empty()) {
|
|
if (changeWriter) {
|
|
// Leading space unneeded as this param will be first.
|
|
newScParams.erase(newScParams.begin());
|
|
writerScParams = fmt::format("{} / (from {}) {}", newScParams,
|
|
origWriterName, writerScParams);
|
|
} else {
|
|
writerScParams.append(newScParams);
|
|
}
|
|
} else {
|
|
writerScParams = newScParams;
|
|
writerScParams.erase(writerScParams.begin()); // Erase leading space.
|
|
}
|
|
|
|
// Add KTXwriterScParams metadata
|
|
updateMetadataValue(KTX_WRITER_SCPARAMS_KEY, writerScParams);
|
|
|
|
// 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);
|
|
}
|
|
|
|
} // namespace ktx
|
|
|
|
KTX_COMMAND_ENTRY_POINT(ktxDeflate, ktx::CommandDeflate)
|