mirror of
https://github.com/KhronosGroup/KTX-Software.git
synced 2026-01-18 17:41:19 +01:00
- Rework navigation among the multiple Doxygen projects for much easier use. - Rename new ktx tool man pages from `ktxtools_*` to `ktx_*` - Add `ktx` tool mainpage based on RELEASE_NOTES info. - Make minor formatting fix in `ktx` man page. - Update acknowledgements. - Remove outdated TODO.md. - Add script to do `$Date$` keyword smudging. Use it in CI and reference it from README.md to avoid repetition of list of files needing smudging. - Add `$Date$` keywords to some docs. - Remove `$Date$` and #ident keywords that are no longer needed or used. - Document the parts of `khr_df.h` relevant to the libktx API.
1999 lines
83 KiB
C++
1999 lines
83 KiB
C++
// -*- tab-width: 4; -*-
|
|
// vi: set sw=2 ts=4 sts=4 expandtab:
|
|
|
|
// Copyright 2010-2020 The Khronos Group Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// To use, download from http://www.billbaxter.com/projects/imdebug/
|
|
// Put imdebug.dll in %SYSTEMROOT% (usually C:\WINDOWS), imdebug.h in
|
|
// ../../include, imdebug.lib in ../../build/msvs/<platform>/vs<ver> &
|
|
// add ..\imdebug.lib to the libraries list in the project properties.
|
|
#define IMAGE_DEBUG 0
|
|
|
|
#include "scapp.h"
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <vector>
|
|
#include <inttypes.h>
|
|
#include <zstd.h>
|
|
|
|
#include "GL/glcorearb.h"
|
|
#include "ktx.h"
|
|
#include "../../lib/vkformat_enum.h"
|
|
#include "argparser.h"
|
|
#include "version.h"
|
|
#include "image.hpp"
|
|
#include "imageio.h"
|
|
#if (IMAGE_DEBUG) && defined(_DEBUG) && defined(_WIN32) && !defined(_WIN32_WCE)
|
|
# include "imdebug.h"
|
|
#elif defined(IMAGE_DEBUG) && IMAGE_DEBUG
|
|
# undef IMAGE_DEBUG
|
|
# define IMAGE_DEBUG 0
|
|
#endif
|
|
|
|
#if !defined(GL_RED)
|
|
#define GL_RED 0x1903
|
|
#define GL_RGB8 0x8051
|
|
#define GL_RGB16 0x8054
|
|
#define GL_RGBA8 0x8058
|
|
#define GL_RGBA16 0x805B
|
|
#endif
|
|
#if !defined(GL_RG)
|
|
#define GL_RG 0x8227
|
|
#define GL_R8 0x8229
|
|
#define GL_R16 0x822A
|
|
#define GL_RG8 0x822B
|
|
#define GL_RG16 0x822C
|
|
#endif
|
|
|
|
#ifndef GL_SR8
|
|
// From GL_EXT_texture_sRGB_R8
|
|
#define GL_SR8 0x8FBD // same as GL_SR8_EXT
|
|
#endif
|
|
|
|
#ifndef GL_SRG8
|
|
// From GL_EXT_texture_sRGB_RG8
|
|
#define GL_SRG8 0x8FBE // same as GL_SRG8_EXT
|
|
#endif
|
|
|
|
enum oetf_e {
|
|
OETF_LINEAR = 0,
|
|
OETF_SRGB = 1,
|
|
OETF_UNSET = 2
|
|
};
|
|
|
|
static ktx_uint32_t log2(ktx_uint32_t v);
|
|
#if IMAGE_DEBUG
|
|
static void dumpImage(_TCHAR* name, int width, int height, int components,
|
|
int componentSize, unsigned char* srcImage);
|
|
#endif
|
|
|
|
using namespace std;
|
|
|
|
/** @page toktx toktx
|
|
@~English
|
|
|
|
Create a KTX file from JPEG, PNG or netpbm format files.
|
|
|
|
@section toktx_synopsis SYNOPSIS
|
|
toktx [options] @e outfile [@e infile.{jpg,png,pam,pgm,ppm} ...]
|
|
|
|
@section toktx_description DESCRIPTION
|
|
Create a Khronos format texture file (KTX) from a set of JPEG (.jpg),
|
|
PNG (.png) or Netpbm format (.pam, .pgm, .ppm) images. It writes the
|
|
destination ktx file to @e outfile, appending ".ktx{,2}" if necessary. If
|
|
@e outfile is '-' the output will be written to stdout.
|
|
|
|
@b toktx reads each named @e infile. which must be in .jpg, .png, .pam,
|
|
.ppm or .pgm format. @e infiles prefixed with '@' are read as text files
|
|
listing actual file names to process with one file path per line. Paths
|
|
must be absolute or relative to the current directory when @b toktx is run.
|
|
If '\@@' is used instead, paths must be absolute or relative to the location
|
|
of the list file.
|
|
|
|
The target texture type (number of components in the output texture) is
|
|
chosen via @b --target_type. Swizzling of the components of the input
|
|
file is specified with @b --input_swizzle and swizzzle metadata can be
|
|
specified with @b --swizzle. Defaults, shown in the following table, are
|
|
based on the components of the input file and whether the target texture
|
|
format is uncompressed or block-compressed including the universal
|
|
formats. Input components are arbitrarily labeled r, g, b & a.
|
|
|
|
| | Uncompressed Formats |||| Block-compressed formats ||||
|
|
| --------------------- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
|
|
| Input components | 1 (greyscale) | 2 (greyscale alpha) | 3 | 4 | 1 | 2 | 3 | 4 |
|
|
| Target type | R | RG | RGB | RGBA | RGB | RGBA | RGB | RGBA |
|
|
| Input swizzle | - | - | - | - | rrr1 | rrrg | - | - |
|
|
| Swizzle | rrr1 | rrrg | - | - | - | - | - | - |
|
|
|
|
As can be seen from the table one- and two-component inputs are treated
|
|
as luminance{,-alpha} in accordance with the JPEG and PNG specifications.
|
|
For consistency Netpbm inputs are handled the same way. Use of R & RG
|
|
types for uncompressed formats saves space but note that the sRGB versions
|
|
of these formats are not widely supported so a warning will be issued
|
|
prompting you to convert the input to linear.
|
|
|
|
The primaries, transfer function (OETF) and the texture's sRGB-ness is set
|
|
based on the input file unless @b --assign_oetf linear or @b --assign_oetf
|
|
srgb is specified. For .jpg files @b toktx always sets BT709/sRGB primaries
|
|
and the sRGB OETF in the output file and creates sRGB format textures.
|
|
Netpbm files always use BT.709/sRGB primaries and the BT.709 OETF. @b toktx
|
|
tranforms these images to the sRGB OETF, sets BT709/sRGB primaries and the
|
|
sRGB OETF in the output file and creates sRGB format textures.
|
|
|
|
For .png files the OETF is set as follows:
|
|
|
|
<dl>
|
|
<dt>No color-info chunks or sRGB chunk present:</dt>
|
|
<dd>primaries are set to BT.709 and OETF to sRGB.</dd>
|
|
<dt>sRGB chunk present:</dt>
|
|
<dd>primaries are set to BT.709 and OETF to sRGB. gAMA and cHRM chunks
|
|
are ignored.</dd>
|
|
<dt>iCCP chunk present:</dt>
|
|
<dd>General ICC profiles are not yet supported by toktx or the KTX2
|
|
format. In future these images may be transformed to linear or sRGB
|
|
OETF as appropriate for the profile. sRGB chunk must not be present.
|
|
</dd>
|
|
<dt>gAMA and/or cHRM chunks present without sRGB or iCCP:</dt>
|
|
<dd>If gAMA is < 60000 the image is transformed to and the OETF is set
|
|
to sRGB. otherwise the image is transformed to and the OETF is set to
|
|
linear. The color primaries in cHRM are matched to one of the
|
|
standard sets listed in the Khronos Data Format Specification (the
|
|
KHR_DF_PRIMARIES values from khr_df.h) and the primaries
|
|
field of the output file's DFD is set to the matched value. If no match
|
|
is found the primaries field is set to UNSPECIFIED.</dd>
|
|
</dl>
|
|
|
|
The following options are always available:
|
|
<dl>
|
|
<dt>--2d</dt>
|
|
<dd>If the image height is 1, by default a KTX file for a 1D texture is
|
|
created. With this option one for a 2D texture is created instead.</dd>
|
|
<dt>--automipmap</dt>
|
|
<dd>Causes the KTX file to be marked to request generation of a mipmap
|
|
pyramid when the file is loaded. This option is mutually exclusive
|
|
with @b --genmipmap, @b --levels and @b --mipmap.</dd>
|
|
<dt>--cubemap</dt>
|
|
<dd>KTX file is for a cubemap. At least 6 @e infiles must be provided,
|
|
more if @b --mipmap or @b --layers is also specified. Provide the
|
|
images in the order +X, -X, +Y, -Y, +Z, -Z where the arrangement is a
|
|
left-handed coordinate system with +Y up. So if you're facing +Z,
|
|
-X will be on your left and +X on your right. If @b --layers > 1 is
|
|
specified, provide the faces for layer 0 first then for layer 1, etc.
|
|
Images must have an upper left origin so --lower_left_maps_to_s0t0
|
|
is ignored with this option.</dd>
|
|
<dt>--depth <number></dt>
|
|
<dd>KTX file is for a 3D texture with a depth of @e number where
|
|
@e number > 0. Provide the file(s) for z=0 first then those for
|
|
z=1, etc. It is an error to specify this together with
|
|
@b --layers or @b --cubemap.</dd>
|
|
<dt>--genmipmap</dt>
|
|
<dd>Causes mipmaps to be generated for each input file. This option is
|
|
mutually exclusive with @b --automipmap and @b --mipmap. When set,
|
|
the following mipmap-generation related options become valid,
|
|
otherwise they are ignored.
|
|
<dl>
|
|
<dt>--filter <name></dt>
|
|
<dd>Specifies the filter to use when generating the mipmaps. @e name
|
|
is a string. The default is @e lanczos4. The following names are
|
|
recognized: @e box, @e tent, @e bell, @e b-spline, @e mitchell,
|
|
@e lanczos3, @e lanczos4, @e lanczos6, @e lanczos12, @e blackman,
|
|
@e kaiser, @e gaussian, @e catmullrom, @e quadratic_interp,
|
|
@e quadratic_approx and @e quadratic_mix.</dd>
|
|
<dt>--fscale <floatVal></dt>
|
|
<dd>The filter scale to use. The default is 1.0.</dd>
|
|
<dt>--wmode <mode></dt>
|
|
<dd>Specify how to sample pixels near the image boundaries. Values
|
|
are @e wrap, @e reflect and @e clamp. The default is @e clamp.</dd>
|
|
</dl>
|
|
</dd>
|
|
<dt>--layers <number></dt>
|
|
<dd>KTX file is for an array texture with @e number of layers where
|
|
@e number > 0. Provide the file(s) for layer 0 first then those
|
|
for layer 1, etc. It is an error to specify this together with
|
|
@b --depth.</dd>
|
|
<dt>--levels <number></dt>
|
|
<dd>KTX file is for a mipmap pyramid with @e number of levels rather than
|
|
a full pyramid. @e number must be > 1 and <= the maximum number
|
|
of levels determined from the size of the base level image. Provide the
|
|
base level image first, if using @b --mipmap. This option is mutually
|
|
exclusive with @b --automipmap.</dd>
|
|
<dt>--mipmap</dt>
|
|
<dd>KTX file is for a mipmap pyramid with one @b infile being explicitly
|
|
provided for each level. Provide the images in the order of layer
|
|
then face or depth slice then level with the base-level image first
|
|
then in order down to the 1x1 image or the level specified by
|
|
@b --levels. @note This ordering differs from that in the
|
|
created texture as it is felt to be more user-friendly.
|
|
|
|
This option is mutually exclusive with @b --automipmap and
|
|
@b --genmipmap.</dd>
|
|
<dt>--nometadata</dt>
|
|
<dd>Do not write KTXorientation metadata into the output file. Metadata
|
|
is written by default. Use of this option is not recommended.</dd>
|
|
<dt>--nowarn</dt>
|
|
<dd>Silence warnings which are issued when certain transformations are
|
|
performed on input images.</dd>
|
|
<dt>--upper_left_maps_to_s0t0</dt>
|
|
<dd>Map the logical upper left corner of the image to s0,t0.
|
|
Although opposite to the OpenGL convention, this is the DEFAULT
|
|
BEHAVIOUR. netpbm and PNG files have an upper left origin so this
|
|
option does not flip the input images. When this option is in effect,
|
|
toktx writes a KTXorientation value of S=r,T=d into the output file
|
|
to inform loaders of the logical orientation. If an OpenGL {,ES}
|
|
loader ignores the orientation value, the image will appear upside
|
|
down.</dd>
|
|
<dt>--lower_left_maps_to_s0t0</dt>
|
|
<dd>Map the logical lower left corner of the image to s0,t0.
|
|
This causes the input netpbm and PNG images to be flipped vertically
|
|
to a lower-left origin. When this option is in effect, toktx
|
|
writes a KTXorientation value of S=r,T=u into the output file
|
|
to inform loaders of the logical orientation. If a Vulkan loader
|
|
ignores the orientation value, the image will appear upside down.
|
|
This option is ignored with @b --cubemap. </dd>
|
|
<dt>--assign_oetf <linear|srgb></dt>
|
|
<dd>Force the created texture to have the specified transfer function. If
|
|
this is specified, implicit or explicit color space information from the
|
|
input file(s) will be ignored and no color transformation will be
|
|
performed. USE WITH CAUTION preferably only when you know the file
|
|
format information is wrong.</dd>
|
|
<dt>--assign_primaries <bt709|none|srgb></dt>
|
|
<dd>Force the created texture to have the specified primaries. If this is
|
|
specified, implicit or explicit color space information from the input
|
|
file(s) will be ignored and no color transformation will be performed.
|
|
USE WITH CAUTION preferably only when you know the file format
|
|
information is wrong.</dd>
|
|
<dt>--convert_oetf <linear|srgb></dt>
|
|
<dd>Convert the input images to the specified transfer function, if the
|
|
current transfer function is different. If both this and
|
|
@b --assign_oetf are specified, conversion will be performed from the
|
|
assigned transfer function to the transfer function specified by this
|
|
option, if different.
|
|
<dt>--linear</dt>
|
|
<dd>Deprecated. Use @b --assign_oetf linear.</dd>
|
|
<dt>--srgb</dt>
|
|
<dd>Deprecated. Use @b --assign_oetf srgb.</dd>
|
|
<dt>--resize <width>x<height>
|
|
<dd>Resize images to @e width X @e height. This should not be used with
|
|
@b --mipmap as it would resize all the images to the same size.
|
|
Resampler options can be set via @b --filter and @b --fscale. </dd>
|
|
<dt>--scale <value></dt>
|
|
<dd>Scale images by @e value as they are read. Resampler options can
|
|
be set via @b --filter and @b --fscale. </dd>.
|
|
<dt>--swizzle <swizzle>
|
|
<dd>Add swizzle metadata to the file being created. @e swizzle
|
|
has the same syntax as the parameter for @b --input_swizzle.
|
|
Not recommended for use with block-cmpressed textures, including
|
|
Basis Universal formats, because something like @c rabb may
|
|
yield drastically different error metrics if done after compression.
|
|
<dt>--target_type <type>
|
|
<dd>Specify the number of components in the created texture. @e type
|
|
is one of the following strings: @c R, @c RG, @c RGB or @c RGBA.
|
|
Excess input components will be dropped. Output components with
|
|
no mapping from the input will be set to 0 or, if the alpha component,
|
|
1.0.
|
|
<dt>--t2</dt>
|
|
<dd>Output in KTX2 format. Default is KTX.</dd>
|
|
</dl>
|
|
@snippet{doc} scapp.h scApp options
|
|
|
|
Options can also be set in the environment variable TOKTX_OPTIONS.
|
|
TOKTX_OPTIONS is parsed first. If conflicting options appear in
|
|
TOKTX_OPTIONS or the command line, the last one seen wins. However if both
|
|
@b --automipmap and @b --mipmap are seen, it is always flagged as an error.
|
|
You can, for example, set TOKTX_OPTIONS=--lower_left_maps_to_s0t0 to change
|
|
the default mapping of the logical image origin to match the GL convention.
|
|
|
|
@section toktx_exitstatus EXIT STATUS
|
|
@b toktx exits 0 on success, 1 on command line errors and 2 on
|
|
functional errors.
|
|
|
|
@section toktx_history HISTORY
|
|
|
|
@par Version 4.0 (using new version numbering system)
|
|
- Add KTX version 2 support including Basis Universal encoding.
|
|
- Add .png and .jpg readers.
|
|
- Transform NetPBM input files to sRGB OETF.
|
|
- Add mipmap generation.
|
|
- Remove legacy items.
|
|
|
|
@par Version 1.3
|
|
- Switch to ktxTexture API.
|
|
- Add --levels option.
|
|
- Add --2d option.
|
|
|
|
@par Version 1.2
|
|
- Remove --sized; always create sized format.
|
|
- Write metadata by default.
|
|
- Bug fixes.
|
|
|
|
@par Version 1.1
|
|
- Moved --alpha and --luminance to legacy.
|
|
|
|
@section toktx_author AUTHOR
|
|
Mark Callow, Edgewise Consulting www.edgewise-consulting.com
|
|
*/
|
|
|
|
#define QUOTE(x) #x
|
|
#define STR(x) QUOTE(x)
|
|
|
|
string myversion(STR(TOKTX_VERSION));
|
|
string mydefversion(STR(TOKTX_DEFAULT_VERSION));
|
|
|
|
class toktxApp : public scApp {
|
|
public:
|
|
toktxApp();
|
|
virtual ~toktxApp() { };
|
|
|
|
virtual int main(int argc, _TCHAR* argv[]);
|
|
virtual void usage();
|
|
|
|
friend void warning(const char *pFmt, va_list args);
|
|
friend void warning(const char *pFmt, ...);
|
|
friend void warning(const string& msg);
|
|
|
|
protected:
|
|
struct targetImageSpec : public ImageSpec {
|
|
OETFFunc decodeFunc = nullptr; // To be applied to the source image!
|
|
OETFFunc encodeFunc = nullptr;
|
|
targetImageSpec& operator=(const ImageSpec& s) {
|
|
*static_cast<ImageSpec*>(this) = s;
|
|
encodeFunc = nullptr;
|
|
decodeFunc = nullptr;
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
virtual bool processOption(argparser& parser, int opt);
|
|
void processEnvOptions();
|
|
void validateOptions();
|
|
|
|
Image* createImage(const targetImageSpec& target, ImageInput& in);
|
|
void convertImageType(Image*& pImage);
|
|
void scaleImage(Image*& pImage, ktx_uint32_t width, ktx_uint32_t height);
|
|
void genMipmap(Image*& pImage,
|
|
uint32_t layer, uint32_t faceSlice,
|
|
ktxTexture* texture);
|
|
|
|
ktxTexture* createTexture(const targetImageSpec& target);
|
|
|
|
void determineTargetColorSpace(const ImageInput& in,
|
|
targetImageSpec& target);
|
|
void determineTargetTypeBitLengthScale(const ImageInput& in,
|
|
targetImageSpec& target,
|
|
string& defaultSwizzle);
|
|
|
|
void determineTargetImageSpec(const ImageInput& in,
|
|
targetImageSpec& target,
|
|
string& defaultSwizzle);
|
|
void checkSpecsMatch(const ImageInput& current, const ImageSpec& firstSpec);
|
|
|
|
void setAstcMode(const targetImageSpec& target);
|
|
|
|
struct commandOptions : public scApp::commandOptions {
|
|
struct mipgenOptions {
|
|
string filter;
|
|
float filterScale;
|
|
enum basisu::Resampler::Boundary_Op wrapMode;
|
|
|
|
mipgenOptions() : filter("lanczos4"), filterScale(1.0),
|
|
wrapMode(basisu::Resampler::Boundary_Op::BOUNDARY_CLAMP) { }
|
|
};
|
|
|
|
int automipmap;
|
|
int cubemap;
|
|
int genmipmap;
|
|
int metadata;
|
|
int mipmap;
|
|
int two_d;
|
|
khr_df_transfer_e convert_oetf;
|
|
khr_df_transfer_e assign_oetf;
|
|
khr_df_primaries_e assign_primaries;
|
|
int useStdin;
|
|
int lower_left_maps_to_s0t0;
|
|
struct mipgenOptions gmopts;
|
|
unsigned int depth;
|
|
unsigned int layers;
|
|
unsigned int levels;
|
|
float scale;
|
|
int resize;
|
|
struct {
|
|
unsigned int width;
|
|
unsigned int height;
|
|
} newGeom;
|
|
string swizzle;
|
|
enum {
|
|
// These values are selected to match the number of components.
|
|
eUnspecified=0, eR=1, eRG, eRGB, eRGBA
|
|
} targetType;
|
|
|
|
commandOptions() {
|
|
automipmap = 0;
|
|
cubemap = 0;
|
|
genmipmap = 0;
|
|
ktx2 = 0;
|
|
metadata = 1;
|
|
mipmap = 0;
|
|
two_d = 0;
|
|
useStdin = 0;
|
|
test = 0;
|
|
depth = 0;
|
|
layers = 0;
|
|
levels = 1;
|
|
convert_oetf = KHR_DF_TRANSFER_UNSPECIFIED;
|
|
assign_oetf = KHR_DF_TRANSFER_UNSPECIFIED;
|
|
assign_primaries = KHR_DF_PRIMARIES_MAX;
|
|
// As required by spec. Opposite of OpenGL {,ES}, same as
|
|
// Vulkan, et al.
|
|
lower_left_maps_to_s0t0 = 0;
|
|
scale = 1.0f;
|
|
resize = 0;
|
|
newGeom.width = newGeom.height = 0;
|
|
targetType = eUnspecified;
|
|
}
|
|
} options;
|
|
|
|
class cant_create_image : public runtime_error {
|
|
using runtime_error::runtime_error;
|
|
};
|
|
|
|
class cant_create_texture : public runtime_error {
|
|
using runtime_error::runtime_error;
|
|
};
|
|
};
|
|
|
|
toktxApp::toktxApp() : scApp(myversion, mydefversion, options)
|
|
{
|
|
argparser::option my_option_list[] = {
|
|
{ "2d", argparser::option::no_argument, &options.two_d, 1 },
|
|
{ "automipmap", argparser::option::no_argument, &options.automipmap, 1 },
|
|
{ "cubemap", argparser::option::no_argument, &options.cubemap, 1 },
|
|
{ "genmipmap", argparser::option::no_argument, &options.genmipmap, 1 },
|
|
{ "filter", argparser::option::required_argument, NULL, 'f' },
|
|
{ "fscale", argparser::option::required_argument, NULL, 'F' },
|
|
{ "wrapping", argparser::option::required_argument, NULL, 'w' },
|
|
{ "depth", argparser::option::required_argument, NULL, 'd' },
|
|
{ "layers", argparser::option::required_argument, NULL, 'a' },
|
|
{ "levels", argparser::option::required_argument, NULL, 'l' },
|
|
{ "mipmap", argparser::option::no_argument, &options.mipmap, 1 },
|
|
{ "nometadata", argparser::option::no_argument, &options.metadata, 0 },
|
|
{ "nowarn", argparser::option::no_argument, &options.warn, 0 },
|
|
{ "lower_left_maps_to_s0t0", argparser::option::no_argument, &options.lower_left_maps_to_s0t0, 1 },
|
|
{ "upper_left_maps_to_s0t0", argparser::option::no_argument, &options.lower_left_maps_to_s0t0, 0 },
|
|
{ "linear", argparser::option::no_argument, (int*)&options.assign_oetf, KHR_DF_TRANSFER_LINEAR },
|
|
{ "srgb", argparser::option::no_argument, (int*)&options.assign_oetf, KHR_DF_TRANSFER_SRGB },
|
|
{ "resize", argparser::option::required_argument, NULL, 'r' },
|
|
{ "scale", argparser::option::required_argument, NULL, 's' },
|
|
{ "swizzle", argparser::option::required_argument, NULL, 1101},
|
|
{ "target_type", argparser::option::required_argument, NULL, 1102},
|
|
{ "convert_oetf", argparser::option::required_argument, NULL, 1103},
|
|
{ "assign_oetf", argparser::option::required_argument, NULL, 1104},
|
|
{ "assign_primaries", argparser::option::required_argument, NULL, 1105},
|
|
{ "t2", argparser::option::no_argument, &options.ktx2, 1},
|
|
};
|
|
|
|
const int lastOptionIndex = sizeof(my_option_list)
|
|
/ sizeof(argparser::option);
|
|
option_list.insert(option_list.begin(), my_option_list,
|
|
my_option_list + lastOptionIndex);
|
|
short_opts += "f:F:w:d:a:l:r:s:";
|
|
}
|
|
|
|
toktxApp theApp;
|
|
|
|
// I really HATE this duplication of text but I cannot find a simple way to
|
|
// avoid it that works on all platforms (e.g running man toktx) even if I was
|
|
// willing to tolerate markup commands in the usage output.
|
|
void
|
|
toktxApp::usage()
|
|
{
|
|
cerr <<
|
|
"Usage: " << name << " [options] <outfile> [<infile>.{jpg,png,pam,pgm,ppm} ...]\n"
|
|
"\n"
|
|
" <outfile> The destination ktx file. \".ktx\" will appended if necessary.\n"
|
|
" If it is '-' the output will be written to stdout.\n"
|
|
" <infile> One or more image files in .jpg, .png, .pam, .ppm, or .pgm\n"
|
|
" format. Other formats can be readily converted to these formats\n"
|
|
" using tools such as ImageMagick and XnView. infiles prefixed\n"
|
|
" with '@' are read as text files listing actual file names to\n"
|
|
" process with one file path per line. Paths must be absolute or\n"
|
|
" relative to the current directory when toktx is run. If '@@'\n"
|
|
" is used instead, paths must be absolute or relative to the\n"
|
|
" location of the list file.\n"
|
|
"\n"
|
|
" The target texture type (number of components in the output texture) is chosen\n"
|
|
" via --target_type. Swizzling of the components of the input file is specified\n"
|
|
" with --input_swizzle and swizzle metadata can be specified with --swizzle\n"
|
|
" Defaults, shown in the following tables, are based on the components of the\n"
|
|
" input file and whether the target texture format is uncompressed or\n"
|
|
" block-compressed including the universal formats. Input components are\n"
|
|
" arbitrarily labeled r, g, b & a.\n"
|
|
"\n"
|
|
" | Uncompressed Formats |\n"
|
|
" |---------------------------------------------------------------------|\n"
|
|
" | Input components | 1 (greyscale) | 2 (greyscale alpha) | 3 | 4 |\n"
|
|
" | Target type | R | RG | RGB | RGBA |\n"
|
|
" | Input swizzle | - | - | - | - |\n"
|
|
" | Swizzle | rrr1 | rrrg | - | - |\n"
|
|
"\n"
|
|
" | Block-compressed formats |\n"
|
|
" |---------------------------------------------------------------------|\n"
|
|
" | Target type | RGB | RGBA | RGB | RGBA |\n"
|
|
" | Input swizzle | rrr1 | rrrg | - | - |\n"
|
|
" | Swizzle | - | - | - | - |\n"
|
|
"\n"
|
|
" As can be seen from the table one- and two-component inputs are treated as\n"
|
|
" luminance{,-alpha} in accordance with the JPEG and PNG specifications. For\n"
|
|
" consistency Netpbm inputs are handled the same way. Use of R & RG types for\n"
|
|
" uncompressed formats saves space but note that the sRGB versions of these\n"
|
|
" formats are not widely supported so a warning will be issued prompting you\n"
|
|
" to convert the input to linear.\n"
|
|
"\n"
|
|
" Options are:\n"
|
|
"\n"
|
|
" --2d If the image height is 1, by default a KTX file for a 1D\n"
|
|
" texture is created. With this option one for a 2D texture is\n"
|
|
" created instead.\n"
|
|
" --automipmap Causes the KTX file to be marked to request generation of a\n"
|
|
" mipmap pyramid when the file is loaded. This option is mutually\n"
|
|
" exclusive with --genmipmap, --levels and --mipmap.\n"
|
|
" --cubemap KTX file is for a cubemap. At least 6 <infile>s must be provided,\n"
|
|
" more if --mipmap is also specified. Provide the images in the\n"
|
|
" order +X, -X, +Y, -Y, +Z, -Z where the arrangement is a\n"
|
|
" left-handed coordinate system with +Y up. So if you're facing +Z,\n"
|
|
" -X will be on your left and +X on your right. If --layers > 1\n"
|
|
" is specified, provide the faces for layer 0 first then for\n"
|
|
" layer 1, etc. Images must have an upper left origin so\n"
|
|
" --lower_left_maps_to_s0t0 is ignored with this option.\n"
|
|
" --depth <number>\n"
|
|
" KTX file is for a 3D texture with a depth of number where\n"
|
|
" number > 0. Provide the file(s) for z=0 first then those for\n"
|
|
" z=1, etc. It is an error to specify this together with\n"
|
|
" --layers or --cubemap.\n"
|
|
" --genmipmap Causes mipmaps to be generated for each input file. This option\n"
|
|
" is mutually exclusive with --automipmap and --mipmap. When set\n"
|
|
" the following mipmap-generation related options become valid,\n"
|
|
" otherwise they are ignored.\n"
|
|
" --filter <name>\n"
|
|
" Specifies the filter to use when generating the mipmaps. name\n"
|
|
" is a string. The default is lanczos4. The following names are\n"
|
|
" recognized: box, tent, bell, b-spline, mitchell, lanczos3\n"
|
|
" lanczos4, lanczos6, lanczos12, blackman, kaiser, gaussian,\n"
|
|
" catmullrom, quadratic_interp, quadratic_approx and\n"
|
|
" quadratic_mix.\n"
|
|
" --fscale <floatVal>\n"
|
|
" The filter scale to use. The default is 1.0.\n"
|
|
" --wmode <mode>\n"
|
|
" Specify how to sample pixels near the image boundaries. Values\n"
|
|
" are wrap, reflect and clamp. The default is clamp.\n"
|
|
" --layers <number>\n"
|
|
" KTX file is for an array texture with number of layers\n"
|
|
" where number > 0. Provide the file(s) for layer 0 first then\n"
|
|
" those for layer 1, etc. It is an error to specify this\n"
|
|
" together with --depth.\n"
|
|
" --levels <number>\n"
|
|
" KTX file is for a mipmap pyramid with <number> of levels rather\n"
|
|
" than a full pyramid. number must be > 1 and <= the maximum number\n"
|
|
" of levels determined from the size of the base image. This option\n"
|
|
" is mutually exclusive with @b --automipmap.\n"
|
|
" --mipmap KTX file is for a mipmap pyramid with one infile being explicitly\n"
|
|
" provided for each level. Provide the images in the order of layer\n"
|
|
" then face or depth slice then level with the base-level image\n"
|
|
" first then in order down to the 1x1 image or the level specified\n"
|
|
" by --levels. This option is mutually exclusive with --automipmap\n"
|
|
" and --genmipmap. Note that this ordering differs from that in the\n"
|
|
" created texture as it is felt to be more user-friendly.\n"
|
|
" --nometadata Do not write KTXorientation metadata into the output file.\n"
|
|
" Use of this option is not recommended.\n"
|
|
" --nowarn Silence warnings which are issued when certain transformations\n"
|
|
" are performed on input images.\n"
|
|
" --upper_left_maps_to_s0t0\n"
|
|
" Map the logical upper left corner of the image to s0,t0.\n"
|
|
" Although opposite to the OpenGL convention, this is the DEFAULT\n"
|
|
" BEHAVIOUR. netpbm and PNG files have an upper left origin so\n"
|
|
" this option does not flip the input images. When this option is\n"
|
|
" in effect, toktx writes a KTXorientation value of S=r,T=d into\n"
|
|
" the output file to inform loaders of the logical orientation. If\n"
|
|
" an OpenGL {,ES} loader ignores the orientation value, the image\n"
|
|
" will appear upside down.\n"
|
|
" --lower_left_maps_to_s0t0\n"
|
|
" Map the logical lower left corner of the image to s0,t0.\n"
|
|
" This causes the input netpbm and PNG images to be flipped\n"
|
|
" vertically to a lower-left origin. When this option is in effect,\n"
|
|
" toktx writes a KTXorientation value of S=r,T=u into the output\n"
|
|
" file to inform loaders of the logical orientation. If a Vulkan\n"
|
|
" loader ignores the orientation value, the image will appear\n"
|
|
" upside down. This option is ignored with --cubemap.\n"
|
|
" --assign_oetf <linear|srgb>\n"
|
|
" Force the created texture to have the specified transfer\n"
|
|
" function. If this is specified, implicit or explicit color space\n"
|
|
" information from the input file(s) will be ignored and no color\n"
|
|
" transformation will be performed. USE WITH CAUTION preferably\n"
|
|
" only when you know the file format information is wrong.\n"
|
|
" --assign_primaries <bt709|none|srgb>\n"
|
|
" Force the created texture to have the specified primaries. If\n"
|
|
" this is specified, implicit or explicit color space information\n"
|
|
" from the input file(s) will be ignored and no color\n"
|
|
" transformation will be performed. USE WITH CAUTION preferably\n"
|
|
" only when you know the file format information is wrong.\n"
|
|
" --convert_oetf <linear|srgb>\n"
|
|
" Convert the input images to the specified transfer function, if\n"
|
|
" the current transfer function is different. If both this and\n"
|
|
" --assign_oetf are specified, conversion will be performed from\n"
|
|
" the assigned transfer function to the transfer function specified\n"
|
|
" by this option, if different.\n"
|
|
" --linear Deprecated. Use --assign_oetf linear.\n"
|
|
" --srgb Deprecated. Use --assign_oetf srgb.\n"
|
|
" --swizzle <swizzle>\n"
|
|
" Add swizzle metadata to the file being created. swizzle has the\n"
|
|
" same syntax as the parameter for --input_swizzle. Not recommended\n"
|
|
" for use with block-cmpressed textures, including Basis Universal\n"
|
|
" formats, because something like `rabb` may yield drastically\n"
|
|
" different error metrics if done after compression.\n"
|
|
" --target_type <type>\n"
|
|
" Specify the number of components in the created texture. type is\n"
|
|
" one of the following strings: @c R, @c RG, @c RGB or @c RGBA.\n"
|
|
" Excess input components will be dropped. Output components with\n"
|
|
" no mapping from the input will be set to 0 or, if the alpha\n"
|
|
" component, 1.0.\n"
|
|
" --resize <width>x<height>\n"
|
|
" Resize images to @e width X @e height. This should not be used\n"
|
|
" with @b--mipmap as it would resize all the images to the same\n"
|
|
" size. Resampler options can be set via --filter and --fscale.\n"
|
|
" --scale <value>\n"
|
|
" Scale images by <value> as they are read. Resampler options can\n"
|
|
" be set via --filter and --fscale.\n"
|
|
" --t2 Output in KTX2 format. Default is KTX.\n";
|
|
scApp::usage();
|
|
cerr << endl <<
|
|
"Options can also be set in the environment variable TOKTX_OPTIONS.\n"
|
|
"TOKTX_OPTIONS is parsed first. If conflicting options appear in TOKTX_OPTIONS\n"
|
|
"or the command line, the last one seen wins. However if both --automipmap and\n"
|
|
"--mipmap are seen, it is always flagged as an error. You can, for example,\n"
|
|
"set TOKTX_OPTIONS=--lower_left_maps_to_s0t0 to change the default mapping of\n"
|
|
"the logical image origin to match the GL convention.\n";
|
|
}
|
|
|
|
int _tmain(int argc, _TCHAR* argv[])
|
|
{
|
|
return theApp.main(argc, argv);
|
|
}
|
|
|
|
int
|
|
toktxApp::main(int argc, _TCHAR *argv[])
|
|
{
|
|
KTX_error_code ret;
|
|
//ktxTextureCreateInfo createInfo;
|
|
ktxTexture* texture = 0;
|
|
int exitCode = 0;
|
|
unsigned int faceSlice, level, layer, levelCount = 1;
|
|
unsigned int levelWidth=0, levelHeight=0, levelDepth=0;
|
|
string defaultSwizzle;
|
|
|
|
processEnvOptions();
|
|
processCommandLine(argc, argv, eAllowStdin, eFirst);
|
|
validateOptions();
|
|
|
|
faceSlice = layer = level = 0;
|
|
vector<_tstring>::const_iterator it;
|
|
bool firstImage = true;
|
|
ImageSpec firstImageSpec;
|
|
targetImageSpec target;
|
|
|
|
for (it = options.infiles.begin(); it < options.infiles.end(); it++) {
|
|
const _tstring& infile = *it;
|
|
Image* image = nullptr;
|
|
uint32_t subimage=0, miplevel=0;
|
|
|
|
try {
|
|
ImageSpec config;
|
|
auto in = ImageInput::open(infile, &config,
|
|
// This lambda provides the trampoline to the
|
|
// warning method.
|
|
[this](const std::string& w) { this->warning(w); });
|
|
|
|
// Input file order is layer, faceSlice, level. This seems easier
|
|
// for a human to manage than the order in a KTX file. It keeps the
|
|
// base level images and their mip levels together. It also works
|
|
// better with subimages and miplevels.
|
|
|
|
// TODO: figure out how to handle 3d input files. slice,level
|
|
// order does not work. Such files will have slices(level0),
|
|
// slices(level1) ...
|
|
if (in->spec().depth() > 1) {
|
|
stringstream message;
|
|
throw cant_create_image(
|
|
"Input of volumetric images is not (yet) supported."
|
|
);
|
|
}
|
|
|
|
do { // subimages
|
|
do { // miplevels
|
|
if (in->miplevelCount() > 1 && !options.mipmap) {
|
|
warning("Ignoring miplevels in %s(%d) because --mipmap not set.",
|
|
infile.c_str(), subimage);
|
|
}
|
|
in->seekSubimage(subimage, miplevel);
|
|
const ImageSpec& spec = in->spec();
|
|
if (!spec.format().sameUnitAllChannels()) {
|
|
throw cant_create_image(
|
|
"Components of differing size or type not yet supported.");
|
|
}
|
|
if (firstImage) {
|
|
if (options.cubemap && spec.width() != spec.height())
|
|
{
|
|
throw cant_create_image(
|
|
"--cubemap specified but image is not square."
|
|
);
|
|
}
|
|
firstImageSpec = spec;
|
|
determineTargetImageSpec(*in, target, defaultSwizzle);
|
|
texture = createTexture(target);
|
|
levelWidth = texture->baseWidth;
|
|
levelHeight = texture->baseHeight;
|
|
levelDepth = texture->baseDepth;
|
|
// Figure out how many levels we'll read from files.
|
|
if (options.mipmap) {
|
|
levelCount = texture->numLevels;
|
|
} else {
|
|
// texture->numLevels will be > 1 for --genmipmap.
|
|
levelCount = 1;
|
|
}
|
|
if (options.astc) {
|
|
setAstcMode(target);
|
|
}
|
|
firstImage = false;
|
|
} else {
|
|
// Subsequent images
|
|
checkSpecsMatch(*in, firstImageSpec);
|
|
// If cubemap we must have passed square test to get
|
|
// here so expected sizes will be square so it is
|
|
// sufficient to test against expected sizes.
|
|
}
|
|
image = createImage(target, *in);
|
|
// Because of potential scale or resize, this test is best
|
|
// done after the image has been created as they will have
|
|
// been applied.
|
|
if (!(image->getWidth() == levelWidth
|
|
&& image->getHeight() == levelHeight))
|
|
// TODO: Figure out 3d input images.
|
|
{
|
|
throw cant_create_image(
|
|
"Image has incorrect size for next layer, face or mip level."
|
|
);
|
|
}
|
|
|
|
//Check astcopts.mode here?
|
|
|
|
if (image->getHeight() > 1 && options.lower_left_maps_to_s0t0) {
|
|
image->yflip();
|
|
}
|
|
if (options.normalize) {
|
|
image->normalize();
|
|
}
|
|
if (options.inputSwizzle.size() > 0
|
|
// inputSwizzle is handled during BasisU and astc encoding
|
|
&& !options.etc1s && !options.bopts.uastc && !options.astc) {
|
|
image->swizzle(options.inputSwizzle);
|
|
}
|
|
|
|
ret = ktxTexture_SetImageFromMemory(texture,
|
|
level,
|
|
layer,
|
|
faceSlice,
|
|
*image,
|
|
image->getByteCount());
|
|
// Only an error in this program could lead to
|
|
// ret != SUCCESS hence no user message.
|
|
assert(ret == KTX_SUCCESS);
|
|
|
|
if (options.genmipmap) {
|
|
genMipmap(image, layer, faceSlice, texture);
|
|
}
|
|
#if IMAGE_DEBUG
|
|
{
|
|
ktx_size_t offset;
|
|
ktxTexture_GetImageOffset(texture, level, 0, faceSlice,
|
|
&offset);
|
|
dumpImage(infile, image->getWidth(), image->getHeight(),
|
|
image->getComponentCount(),
|
|
image->getComponentSize(),
|
|
texture.pData + offset);
|
|
}
|
|
#endif
|
|
delete image;
|
|
image = nullptr;
|
|
|
|
miplevel++;
|
|
level++;
|
|
levelWidth = maximum(levelWidth >> 1, 1U);
|
|
levelHeight = maximum(levelHeight >> 1, 1U);
|
|
levelDepth = maximum(levelDepth >> 1, 1U);
|
|
} while (miplevel < levelCount && miplevel < in->miplevelCount());
|
|
subimage++;
|
|
if (level == levelCount) {
|
|
faceSlice++;
|
|
level = 0;
|
|
levelWidth = texture->baseWidth;
|
|
levelHeight = texture->baseHeight;
|
|
levelDepth = texture->baseDepth;
|
|
if (faceSlice == (options.cubemap ? 6 : levelDepth)) {
|
|
faceSlice = 0;
|
|
layer++;
|
|
if (layer == texture->numLayers) {
|
|
// We're done.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} while (subimage < in->subimageCount());
|
|
if (subimage < in->subimageCount()) {
|
|
; // warn unused
|
|
}
|
|
if (layer == texture->numLayers) {
|
|
// We're done.
|
|
break;
|
|
}
|
|
} catch (cant_create_texture& e) {
|
|
cerr << name << ": failed to create ktxTexture. "
|
|
<< e.what() << endl;
|
|
exit(2);
|
|
} catch (cant_create_image& e) {
|
|
cerr << name << ": could not create image from "
|
|
<< infile << "(" << subimage << "," << miplevel
|
|
<< ")." << endl << e.what() << endl;
|
|
// Some of these exceptions are thrown after the image has
|
|
// been created despite its name. We want the same message
|
|
// to the user hence not creating a different exception.
|
|
if (image != nullptr)
|
|
delete image;
|
|
exitCode = 1;
|
|
goto cleanup;
|
|
} catch (runtime_error& e) {
|
|
cerr << name << ": failed to create image from "
|
|
<< infile << "(" << subimage << "," << miplevel
|
|
<< ")." << endl << e.what() << endl;
|
|
exitCode = 2;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
if (layer != texture->numLayers) {
|
|
cerr << name << ": too few input images for " << levelCount
|
|
<< " levels, " << texture->numLayers
|
|
<< " layers and " << texture->numFaces
|
|
<< " faces." << endl;
|
|
exitCode = 1;
|
|
goto cleanup;
|
|
}
|
|
// We break out of the loop when done so final iterator increment
|
|
// never happens hence -1 here.
|
|
if (it != options.infiles.end() - 1) {
|
|
warning("Ignoring excess input images.");
|
|
}
|
|
|
|
/*
|
|
* Add orientation metadata.
|
|
*/
|
|
if (options.metadata) {
|
|
ktxHashList* ht = &texture->kvDataHead;
|
|
char orientation[20];
|
|
if (options.ktx2) {
|
|
orientation[0] = 'r';
|
|
if (texture->numDimensions > 1) {
|
|
orientation[1] = options.lower_left_maps_to_s0t0 ? 'u' : 'd';
|
|
if (texture->numDimensions > 2) {
|
|
orientation[2] = options.lower_left_maps_to_s0t0
|
|
? 'o' : 'i';
|
|
orientation[3] = 0;
|
|
} else {
|
|
orientation[2] = 0;
|
|
}
|
|
} else {
|
|
orientation[1] = 0;
|
|
}
|
|
} else {
|
|
assert(strlen(KTX_ORIENTATION3_FMT) < sizeof(orientation));
|
|
if (texture->numDimensions == 1) {
|
|
snprintf(orientation, sizeof(orientation), KTX_ORIENTATION1_FMT,
|
|
'r');
|
|
} else if (texture->numDimensions == 2) {
|
|
snprintf(orientation, sizeof(orientation), KTX_ORIENTATION2_FMT,
|
|
'r', options.lower_left_maps_to_s0t0 ? 'u' : 'd');
|
|
} else
|
|
snprintf(orientation, sizeof(orientation), KTX_ORIENTATION3_FMT,
|
|
'r', options.lower_left_maps_to_s0t0 ? 'u' : 'd',
|
|
options.lower_left_maps_to_s0t0 ? 'o' : 'i');
|
|
}
|
|
ktxHashList_AddKVPair(ht, KTX_ORIENTATION_KEY,
|
|
(unsigned int)strlen(orientation) + 1,
|
|
orientation);
|
|
}
|
|
if (options.ktx2) {
|
|
// Add required writer metadata.
|
|
stringstream writer;
|
|
writeId(writer, options.test != 0);
|
|
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
|
|
(ktx_uint32_t)writer.str().length() + 1,
|
|
writer.str().c_str());
|
|
|
|
string swizzle;
|
|
// Add Swizzle metadata
|
|
if (options.swizzle.size()) {
|
|
swizzle = options.swizzle;
|
|
} else if (!options.etc1s && !options.bopts.uastc && !options.astc
|
|
&& defaultSwizzle.size()) {
|
|
swizzle = defaultSwizzle;
|
|
}
|
|
if (swizzle.size()) {
|
|
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_SWIZZLE_KEY,
|
|
(uint32_t)swizzle.size()+1,
|
|
// +1 is for the NUL on the c_str
|
|
swizzle.c_str());
|
|
}
|
|
}
|
|
|
|
FILE* f;
|
|
if (options.outfile.compare("-") == 0) {
|
|
f = stdout;
|
|
#if defined(_WIN32)
|
|
/* Set "stdout" to have binary mode */
|
|
(void)_setmode( _fileno( stdout ), _O_BINARY );
|
|
#endif
|
|
} else
|
|
f = _tfopen(options.outfile.c_str(), "wb");
|
|
|
|
if (f) {
|
|
if (options.astc || options.etc1s || options.bopts.uastc || options.zcmp) {
|
|
string& swizzle = options.inputSwizzle.size() == 0 && defaultSwizzle.size() && !options.normalMode
|
|
? defaultSwizzle
|
|
: options.inputSwizzle;
|
|
exitCode = encode((ktxTexture2*)texture, swizzle,
|
|
f == stdout ? "stdout" : options.outfile);
|
|
if (exitCode)
|
|
goto closefileandcleanup;
|
|
}
|
|
ret = ktxTexture_WriteToStdioStream(ktxTexture(texture), f);
|
|
if (KTX_SUCCESS != ret) {
|
|
cerr << name << ": "
|
|
<< "%s failed to write KTX file \"" << options.outfile
|
|
<< "\"; KTX error: " << ktxErrorString(ret) << endl;
|
|
exitCode = 2;
|
|
}
|
|
closefileandcleanup:
|
|
fclose(f);
|
|
if (exitCode && (f != stdout)) {
|
|
_tunlink(options.outfile.c_str());
|
|
}
|
|
} else {
|
|
cerr << name << ": "
|
|
<< "could not open output file \"" << options.outfile
|
|
<< "\". " << strerror(errno) << endl;
|
|
exitCode = 2;
|
|
}
|
|
|
|
cleanup:
|
|
if (texture) ktxTexture_Destroy(ktxTexture(texture));
|
|
return exitCode;
|
|
}
|
|
|
|
Image*
|
|
toktxApp::createImage(const targetImageSpec& target, ImageInput& in)
|
|
{
|
|
const ImageSpec& inSpec = in.spec();
|
|
|
|
Image* image = nullptr;
|
|
if (target.format().channelBitLength() == 16) {
|
|
switch (inSpec.format().channelCount()) {
|
|
case 1: {
|
|
image = new r16image(inSpec.width(), inSpec.height());
|
|
break;
|
|
} case 2: {
|
|
image = new rg16image(inSpec.width(), inSpec.height());
|
|
break;
|
|
} case 3: {
|
|
image = new rgb16image(inSpec.width(), inSpec.height());
|
|
break;
|
|
} case 4: {
|
|
image = new rgba16image(inSpec.width(), inSpec.height());
|
|
break;
|
|
}
|
|
}
|
|
} else if (target.format().channelBitLength() == 8) {
|
|
switch (inSpec.format().channelCount()) {
|
|
case 1: {
|
|
image = new r8image(inSpec.width(), inSpec.height());
|
|
break;
|
|
} case 2: {
|
|
image = new rg8image(inSpec.width(), inSpec.height());
|
|
break;
|
|
} case 3: {
|
|
image = new rgb8image(inSpec.width(), inSpec.height());
|
|
break;
|
|
} case 4: {
|
|
image = new rgba8image(inSpec.width(), inSpec.height());
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
stringstream message;
|
|
uint32_t ct = inSpec.format().samples[0].channelType;
|
|
khr_df_sample_datatype_qualifiers_e dtq;
|
|
dtq = static_cast<khr_df_sample_datatype_qualifiers_e>(ct);
|
|
message << "Unsupported format "
|
|
<< inSpec.format().channelBitLength()
|
|
<< "-bit " << dtq << " needed.";
|
|
throw runtime_error(message.str());
|
|
// TODO: uint32, uint64, float etc.
|
|
}
|
|
|
|
in.readImage(static_cast<uint8_t*>(*image), image->getByteCount(),
|
|
0, inSpec.format().channelCount(), target.format());
|
|
/* Sanity check. */
|
|
assert(image->getWidth() * image->getHeight() * image->getPixelSize()
|
|
== image->getByteCount());
|
|
|
|
// TODO: Convert primaries?
|
|
image->setPrimaries((khr_df_primaries_e)target.format().primaries());
|
|
if (target.encodeFunc != nullptr) {
|
|
assert(target.decodeFunc != nullptr);
|
|
image->transformOETF(target.decodeFunc, target.encodeFunc,
|
|
inSpec.format().oeGamma());
|
|
if (target.encodeFunc == encode_sRGB) {
|
|
image->setOetf(KHR_DF_TRANSFER_SRGB);
|
|
} else {
|
|
image->setOetf(KHR_DF_TRANSFER_LINEAR);
|
|
}
|
|
} else {
|
|
image->setOetf((khr_df_transfer_e)target.format().transfer());
|
|
}
|
|
|
|
if (options.scale != 1.0f) {
|
|
scaleImage(image,
|
|
static_cast<ktx_uint32_t>(image->getWidth() * options.scale),
|
|
static_cast<ktx_uint32_t>(image->getHeight() * options.scale));
|
|
} else if (options.resize
|
|
&& (image->getWidth() != target.width()
|
|
|| image->getHeight() != target.height()))
|
|
{
|
|
// --resize is not allowed with --mipmap so createImage will never be
|
|
// called for other than the base level when set. This would be
|
|
// incorrect otherwise. target reflects the resize value, if any.
|
|
scaleImage(image, target.width(), target.height());
|
|
}
|
|
if (options.targetType != commandOptions::eUnspecified) {
|
|
convertImageType(image);
|
|
}
|
|
return image;
|
|
}
|
|
|
|
void
|
|
toktxApp::convertImageType(Image*& pImage)
|
|
{
|
|
// TODO: These copyTo's should be reversed. The image should have
|
|
// a copy constructor for each componentCount src image.
|
|
if (options.targetType != (int)pImage->getComponentCount()) {
|
|
Image* newImage = nullptr;
|
|
// The casts in the following copyTo* definitions only work
|
|
// because, thanks to the switch, at runtime we always pass
|
|
// the image type being cast to.
|
|
if (pImage->getComponentSize() == 2) {
|
|
switch (options.targetType) {
|
|
case commandOptions::eR:
|
|
newImage = new r16image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToR(*newImage);
|
|
break;
|
|
case commandOptions::eRG:
|
|
newImage = new rg16image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToRG(*newImage);
|
|
break;
|
|
case commandOptions::eRGB:
|
|
newImage = new rgb16image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToRGB(*newImage);
|
|
break;
|
|
case commandOptions::eRGBA:
|
|
newImage = new rgba16image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToRGBA(*newImage);
|
|
break;
|
|
case commandOptions::eUnspecified:
|
|
assert(false);
|
|
}
|
|
} else {
|
|
switch (options.targetType) {
|
|
case commandOptions::eR:
|
|
newImage = new r8image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToR(*newImage);
|
|
break;
|
|
case commandOptions::eRG:
|
|
newImage = new rg8image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToRG(*newImage);
|
|
break;
|
|
case commandOptions::eRGB:
|
|
newImage = new rgb8image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToRGB(*newImage);
|
|
break;
|
|
case commandOptions::eRGBA:
|
|
newImage = new rgba8image(pImage->getWidth(), pImage->getHeight());
|
|
pImage->copyToRGBA(*newImage);
|
|
break;
|
|
case commandOptions::eUnspecified:
|
|
assert(false);
|
|
}
|
|
}
|
|
if (newImage) {
|
|
delete pImage;
|
|
pImage = newImage;
|
|
} else {
|
|
throw runtime_error(
|
|
"Out of memory for image with new target type."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: This should probably be a method on Image.
|
|
void
|
|
toktxApp::scaleImage(Image*& pImage, ktx_uint32_t width, ktx_uint32_t height)
|
|
{
|
|
Image* pScaledImage = pImage->createImage(width, height);
|
|
|
|
try {
|
|
pImage->resample(*pScaledImage,
|
|
pImage->getOetf() == KHR_DF_TRANSFER_SRGB,
|
|
options.gmopts.filter.c_str(),
|
|
options.gmopts.filterScale,
|
|
basisu::Resampler::Boundary_Op::BOUNDARY_CLAMP);
|
|
} catch (runtime_error& e) {
|
|
stringstream message;
|
|
message << "Image::resample() failed! " << e.what();
|
|
// A couple of the exceptions have to do with memory but the
|
|
// others are "too large an image" and "unknown filter." The
|
|
// latter are much more likely to occur hence choice of exception.
|
|
throw cant_create_image(message.str());
|
|
}
|
|
pScaledImage->setOetf(pImage->getOetf());
|
|
pScaledImage->setPrimaries(pImage->getPrimaries());
|
|
delete pImage;
|
|
pImage = pScaledImage;
|
|
}
|
|
|
|
void
|
|
toktxApp::genMipmap(Image*& pImage,
|
|
uint32_t layer, uint32_t faceSlice,
|
|
ktxTexture* texture)
|
|
{
|
|
for (uint32_t glevel = 1; glevel < texture->numLevels; glevel++) {
|
|
Image *levelImage = pImage->createImage(
|
|
maximum<uint32_t>(1, pImage->getWidth() >> glevel),
|
|
maximum<uint32_t>(1, pImage->getHeight() >> glevel));
|
|
levelImage->setOetf(pImage->getOetf());
|
|
levelImage->setPrimaries(pImage->getPrimaries());
|
|
try {
|
|
pImage->resample(*levelImage,
|
|
pImage->getOetf() == KHR_DF_TRANSFER_SRGB,
|
|
options.gmopts.filter.c_str(),
|
|
options.gmopts.filterScale,
|
|
options.gmopts.wrapMode);
|
|
} catch (runtime_error& e) {
|
|
stringstream message;
|
|
message << "Image::resample() failed! " << e.what();
|
|
cant_create_image(message.str());
|
|
}
|
|
|
|
if (options.normalize)
|
|
levelImage->normalize();
|
|
|
|
MAYBE_UNUSED ktx_error_code_e ret;
|
|
ret = ktxTexture_SetImageFromMemory(texture,
|
|
glevel,
|
|
layer,
|
|
faceSlice,
|
|
*levelImage,
|
|
levelImage->getByteCount());
|
|
assert(ret == KTX_SUCCESS);
|
|
delete levelImage;
|
|
}
|
|
}
|
|
|
|
ktxTexture*
|
|
toktxApp::createTexture(const targetImageSpec& target)
|
|
{
|
|
ktxTextureCreateInfo createInfo;
|
|
ktxTexture* texture = 0;
|
|
|
|
memset(&createInfo, 0, sizeof(createInfo));
|
|
|
|
if (options.cubemap)
|
|
createInfo.numFaces = 6;
|
|
else
|
|
createInfo.numFaces = 1;
|
|
|
|
if (options.layers) {
|
|
createInfo.numLayers = options.layers;
|
|
createInfo.isArray = KTX_TRUE;
|
|
} else {
|
|
createInfo.numLayers = 1;
|
|
createInfo.isArray = KTX_FALSE;
|
|
}
|
|
|
|
bool srgb = (target.format().transfer() == KHR_DF_TRANSFER_SRGB);
|
|
uint32_t componentCount = target.format().channelCount();
|
|
switch (componentCount) {
|
|
case 1:
|
|
switch (target.format().channelBitLength()) {
|
|
case 8:
|
|
createInfo.glInternalformat
|
|
= srgb ? GL_SR8 : GL_R8;
|
|
createInfo.vkFormat
|
|
= srgb ? VK_FORMAT_R8_SRGB
|
|
: VK_FORMAT_R8_UNORM;
|
|
break;
|
|
case 16:
|
|
createInfo.glInternalformat = GL_R16;
|
|
createInfo.vkFormat = VK_FORMAT_R16_UNORM;
|
|
break;
|
|
case 32:
|
|
createInfo.glInternalformat = GL_R32F;
|
|
createInfo.vkFormat = VK_FORMAT_R32_SFLOAT;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 2:
|
|
switch (target.format().channelBitLength()) {
|
|
case 8:
|
|
createInfo.glInternalformat
|
|
= srgb ? GL_SRG8 : GL_RG8;
|
|
createInfo.vkFormat
|
|
= srgb ? VK_FORMAT_R8G8_SRGB
|
|
: VK_FORMAT_R8G8_UNORM;
|
|
break;
|
|
case 16:
|
|
createInfo.glInternalformat = GL_RG16;
|
|
createInfo.vkFormat = VK_FORMAT_R16G16_UNORM;
|
|
break;
|
|
case 32:
|
|
createInfo.glInternalformat = GL_RG32F;
|
|
createInfo.vkFormat = VK_FORMAT_R32G32_SFLOAT;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
switch (target.format().channelBitLength()) {
|
|
case 8:
|
|
createInfo.glInternalformat
|
|
= srgb ? GL_SRGB8 : GL_RGB8;
|
|
createInfo.vkFormat
|
|
= srgb ? VK_FORMAT_R8G8B8_SRGB
|
|
: VK_FORMAT_R8G8B8_UNORM;
|
|
break;
|
|
case 16:
|
|
createInfo.glInternalformat = GL_RGB16;
|
|
createInfo.vkFormat = VK_FORMAT_R16G16B16_UNORM;
|
|
break;
|
|
case 32:
|
|
createInfo.glInternalformat = GL_RGB32F;
|
|
createInfo.vkFormat = VK_FORMAT_R32G32B32_SFLOAT;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
switch (target.format().channelBitLength()) {
|
|
case 8:
|
|
createInfo.glInternalformat
|
|
= srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8;
|
|
createInfo.vkFormat
|
|
= srgb ? VK_FORMAT_R8G8B8A8_SRGB
|
|
: VK_FORMAT_R8G8B8A8_UNORM;
|
|
break;
|
|
case 16:
|
|
createInfo.glInternalformat = GL_RGBA16;
|
|
createInfo.vkFormat = VK_FORMAT_R16G16B16A16_UNORM;
|
|
break;
|
|
case 32:
|
|
createInfo.glInternalformat = GL_RGBA32F;
|
|
createInfo.vkFormat = VK_FORMAT_R32G32B32A32_SFLOAT;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
/* If we get here there's a bug. */
|
|
assert(0);
|
|
}
|
|
if ((createInfo.vkFormat == VK_FORMAT_R8_SRGB
|
|
|| createInfo.vkFormat == VK_FORMAT_R8G8_SRGB)
|
|
&& !(options.astc || options.etc1s || options.bopts.uastc)) {
|
|
// Encoding to BasisU or ASTC will cause conversion to RGB.
|
|
warning("GPU support of sRGB variants of R & RG formats is"
|
|
" limited.\nConsider using '--target_type' or"
|
|
" '--convert_oetf linear' to avoid these formats.");
|
|
}
|
|
createInfo.baseWidth = target.width();
|
|
createInfo.baseHeight = target.height();
|
|
createInfo.baseDepth = options.depth ? options.depth : 1;
|
|
if (options.depth > 0) {
|
|
// In this case, don't care about image->getHeight(). Images are
|
|
// always considered to be 2d. No need to set options.two_d.
|
|
createInfo.numDimensions = 3;
|
|
} else if (target.height() == 1 && !options.two_d)
|
|
createInfo.numDimensions = 1;
|
|
else
|
|
createInfo.numDimensions = 2;
|
|
if (options.automipmap) {
|
|
createInfo.numLevels = 1;
|
|
createInfo.generateMipmaps = KTX_TRUE;
|
|
} else {
|
|
createInfo.generateMipmaps = KTX_FALSE;
|
|
if (options.mipmap || options.genmipmap) {
|
|
// Calculate number of miplevels
|
|
GLuint max_dim = maximum(target.width(), target.height());
|
|
createInfo.numLevels = log2(max_dim) + 1;
|
|
if (options.levels > 1) {
|
|
if (options.levels > createInfo.numLevels) {
|
|
stringstream message;
|
|
message << "--levels value " << options.levels
|
|
<< " is greater than the maximum"
|
|
<< " levels possible for the image size "
|
|
<< createInfo.numLevels << ".";
|
|
throw cant_create_image(message.str());
|
|
}
|
|
// Override the above.
|
|
createInfo.numLevels = options.levels;
|
|
}
|
|
} else {
|
|
createInfo.numLevels = 1;
|
|
}
|
|
}
|
|
|
|
ktx_error_code_e ret;
|
|
if (options.ktx2) {
|
|
ret = ktxTexture2_Create(&createInfo,
|
|
KTX_TEXTURE_CREATE_ALLOC_STORAGE,
|
|
(ktxTexture2**)&texture);
|
|
} else {
|
|
ret = ktxTexture1_Create(&createInfo,
|
|
KTX_TEXTURE_CREATE_ALLOC_STORAGE,
|
|
(ktxTexture1**)&texture);
|
|
}
|
|
if (KTX_SUCCESS != ret) {
|
|
stringstream message;
|
|
message << "libktx error: " << ktxErrorString(ret);
|
|
throw cant_create_texture(message.str());
|
|
}
|
|
|
|
// BT709 is the default for DFDs.
|
|
if (options.ktx2 && target.format().primaries() != KHR_DF_PRIMARIES_BT709) {
|
|
KHR_DFDSETVAL(((ktxTexture2*)texture)->pDfd + 1, PRIMARIES,
|
|
target.format().primaries());
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
void
|
|
toktxApp::determineTargetColorSpace(const ImageInput& in, targetImageSpec& target)
|
|
{
|
|
// 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.
|
|
const ImageSpec& spec = in.spec();
|
|
// Set Primaries
|
|
if (options.assign_primaries != KHR_DF_PRIMARIES_MAX) {
|
|
target.format().setPrimaries(options.assign_primaries);
|
|
} else if (spec.format().primaries() != KHR_DF_PRIMARIES_UNSPECIFIED) {
|
|
target.format().setPrimaries(spec.format().primaries());
|
|
} else {
|
|
// Leave as unspecified.
|
|
target.format().setPrimaries(spec.format().primaries());
|
|
}
|
|
|
|
// OETF / Transfer function handling in priority order:
|
|
//
|
|
// 1. Use assign_oetf option value, if set.
|
|
// 2. Use OETF signalled by plugin, if LINEAR or SRGB. If ITU signalled,
|
|
// set up conversion to SRGB. For all others, throw error.
|
|
// 3. If ICC profile signalled, throw error.
|
|
// 4. If gamma of 1.0 signalled use LINEAR. If gamma of .45454 signalled,
|
|
// set up for conversion to SRGB. If gamma of 0.0 is signalled,
|
|
// set SRGB. For any other gamma value, throw error.
|
|
// 5. If no color info is signalled, and input is PNG follow W3C
|
|
// recommendation of sRGB. For other input formats throw error.
|
|
// 6. Convert OETF based on convert_oetf option value or as described
|
|
// above.
|
|
//
|
|
if (options.assign_oetf != KHR_DF_TRANSFER_UNSPECIFIED) {
|
|
target.format().setTransfer(options.assign_oetf);
|
|
} else {
|
|
// Set image's OETF as indicated by metadata.
|
|
if (spec.format().transfer() != KHR_DF_TRANSFER_UNSPECIFIED) {
|
|
target.format().setTransfer(spec.format().transfer());
|
|
switch (spec.format().transfer()) {
|
|
case KHR_DF_TRANSFER_LINEAR:
|
|
target.decodeFunc = decode_linear;
|
|
break;
|
|
case KHR_DF_TRANSFER_SRGB:
|
|
target.decodeFunc = decode_sRGB;
|
|
break;
|
|
case KHR_DF_TRANSFER_ITU:
|
|
target.format().setTransfer(KHR_DF_TRANSFER_SRGB);
|
|
target.decodeFunc = decode_bt709;
|
|
target.encodeFunc = encode_sRGB;
|
|
break;
|
|
default:
|
|
throw cant_create_image(
|
|
"Transfer function not supported by KTX."
|
|
" Use --assign_oetf to specify a different one.");
|
|
}
|
|
} else if (spec.format().iccProfileName().size()) {
|
|
throw cant_create_image(
|
|
"It has an ICC profile. These are not supported."
|
|
" Use --assign_oetf to specify handling.");
|
|
} else if (spec.format().oeGamma() >= 0.0f) {
|
|
target.decodeFunc = decode_gamma;
|
|
if (spec.format().oeGamma() > .45450f
|
|
&& spec.format().oeGamma() < .45460f) {
|
|
// N.B The previous loader matched oeGamma .45455 to the sRGB
|
|
// OETF and did not do an OETF transformation. In this loader
|
|
// we decode and reencode. Previous behavior can be obtained
|
|
// with the --assign_oetf option to toktx.
|
|
//
|
|
// 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.
|
|
target.format().setTransfer(KHR_DF_TRANSFER_SRGB);
|
|
target.encodeFunc = encode_sRGB;
|
|
} else if (spec.format().oeGamma() == 1.0) {
|
|
target.format().setTransfer(KHR_DF_TRANSFER_LINEAR);
|
|
} else if (spec.format().oeGamma() == 0.0f) {
|
|
if (!in.formatName().compare("png")) {
|
|
warning("Ignoring reported gamma of 0.0f in %s."
|
|
"Handling as sRGB.", in.filename().c_str());
|
|
target.format().setTransfer(KHR_DF_TRANSFER_SRGB);
|
|
target.decodeFunc = decode_sRGB;
|
|
} else {
|
|
throw cant_create_image("Its reported gamma is 0.0f."
|
|
" Use --assign_oetf to specify handling.");
|
|
}
|
|
} else {
|
|
if (options.convert_oetf == KHR_DF_TRANSFER_UNSPECIFIED) {
|
|
stringstream message;
|
|
message << "Its encoding gamma, "
|
|
<< spec.format().oeGamma()
|
|
<< ", is not automatically supported by KTX." << endl
|
|
<< "Specify handling with --convert_oetf or"
|
|
<< " --assign_oetf.";
|
|
throw cant_create_image(message.str());
|
|
}
|
|
}
|
|
} else {
|
|
if (!in.formatName().compare("png")) {
|
|
// Follow W3C. Treat unspecified as sRGB.
|
|
target.format().setTransfer(KHR_DF_TRANSFER_SRGB);
|
|
target.decodeFunc = decode_sRGB;
|
|
} else {
|
|
throw cant_create_image(
|
|
"It has no color space information."
|
|
" Use --assign_oetf to specify handling.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.convert_oetf != KHR_DF_TRANSFER_UNSPECIFIED &&
|
|
options.convert_oetf != spec.format().transfer()) {
|
|
if (options.convert_oetf == KHR_DF_TRANSFER_SRGB) {
|
|
target.encodeFunc = encode_sRGB;
|
|
target.format().setTransfer(KHR_DF_TRANSFER_SRGB);
|
|
} else {
|
|
target.encodeFunc = encode_linear;
|
|
target.format().setTransfer(KHR_DF_TRANSFER_LINEAR);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
toktxApp::determineTargetTypeBitLengthScale(const ImageInput& in,
|
|
targetImageSpec& target,
|
|
string& defaultSwizzle)
|
|
{
|
|
const FormatDescriptor& format = in.spec().format();
|
|
FormatDescriptor& targetFormat = target.format();
|
|
uint32_t bitLength = format.channelBitLength();
|
|
uint32_t maxValue;
|
|
|
|
if (format.channelBitLength() > 8
|
|
&& (options.etc1s || options.bopts.uastc)) {
|
|
bitLength = 8;
|
|
} else if (format.channelBitLength() < 8) {
|
|
bitLength = 8;
|
|
}
|
|
|
|
// Currently we only support unsigned normalized input formats.
|
|
maxValue = ((1U << bitLength) - 1U);
|
|
|
|
// TODO: Support < 8 bit channels for non-block-compressed?
|
|
|
|
if (targetFormat.channelBitLength()
|
|
!= format.channelBitLength()) {
|
|
warning("Rescaling %d-bit image in %s to %d bits.",
|
|
format.channelBitLength(),
|
|
in.filename().c_str(),
|
|
targetFormat.channelBitLength());
|
|
}
|
|
|
|
uint32_t channelCount = format.channelCount();
|
|
if (options.targetType != commandOptions::eUnspecified) {
|
|
channelCount = options.targetType;
|
|
targetFormat.setModel(KHR_DF_MODEL_RGBSDA);
|
|
} else if (format.model() == KHR_DF_MODEL_YUVSDA) {
|
|
// It's a luminance image. Override.
|
|
assert(format.channelCount() < 3);
|
|
targetFormat.setModel(KHR_DF_MODEL_RGBSDA);
|
|
if (format.channelCount() == 1) {
|
|
defaultSwizzle = "rrr1";
|
|
} else {
|
|
defaultSwizzle = "rrrg";
|
|
}
|
|
}
|
|
|
|
// Must be after setting of model.
|
|
if (bitLength != targetFormat.channelBitLength()
|
|
|| maxValue != targetFormat.channelUpper()
|
|
|| channelCount != targetFormat.channelCount())
|
|
{
|
|
targetFormat.updateSampleInfo(channelCount, bitLength, 0, maxValue,
|
|
targetFormat.channelDataType());
|
|
}
|
|
}
|
|
|
|
void
|
|
toktxApp::determineTargetImageSpec(const ImageInput& in,
|
|
targetImageSpec& target,
|
|
string& defaultSwizzle)
|
|
{
|
|
target = in.spec();
|
|
if (options.scale != 1.0f) {
|
|
target.setWidth(
|
|
static_cast<ktx_uint32_t>(target.width() * options.scale));
|
|
target.setHeight(
|
|
static_cast<ktx_uint32_t>(target.height() * options.scale));
|
|
target.setDepth(
|
|
static_cast<ktx_uint32_t>(target.depth() * options.scale));
|
|
} else if (options.resize) {
|
|
target.setWidth(options.newGeom.width);
|
|
target.setHeight(options.newGeom.height);
|
|
// Current CLI does not allow for setting depth.
|
|
}
|
|
determineTargetTypeBitLengthScale(in, target, defaultSwizzle);
|
|
determineTargetColorSpace(in, target);
|
|
}
|
|
|
|
void
|
|
toktxApp::checkSpecsMatch(const ImageInput& currentFile,
|
|
const ImageSpec& firstSpec)
|
|
{
|
|
const FormatDescriptor firstFormat = firstSpec.format();
|
|
const FormatDescriptor& currentFormat = currentFile.spec().format();
|
|
if (currentFormat.transfer() != firstFormat.transfer()
|
|
&& options.convert_oetf == KHR_DF_TRANSFER_UNSPECIFIED)
|
|
{
|
|
stringstream msg;
|
|
if (options.assign_oetf == KHR_DF_TRANSFER_UNSPECIFIED) {
|
|
msg << "Image";
|
|
} else {
|
|
msg << "Image in " << currentFile.filename() << "("
|
|
<< currentFile.currentSubimage() << ","
|
|
<< currentFile.currentMiplevel() << ")";
|
|
}
|
|
msg << " has a different transfer function (OETF)"
|
|
<< " than preceding image(s).";
|
|
if (options.assign_oetf == KHR_DF_TRANSFER_UNSPECIFIED) {
|
|
msg << endl
|
|
<< "Use --assign_oetf (not recommended) or --convert_oetf to"
|
|
<< " stop this error.";
|
|
throw cant_create_image(msg.str());
|
|
} else {
|
|
warning(msg.str());
|
|
}
|
|
// Don't warn when convert_oetf is set as proper conversions
|
|
// will be done so all images will be in the same space.
|
|
}
|
|
if (currentFormat.primaries() != firstFormat.primaries()) {
|
|
stringstream msg;
|
|
if (options.assign_primaries == KHR_DF_PRIMARIES_UNSPECIFIED) {
|
|
msg << "Image";
|
|
} else{
|
|
msg << "Image in " << currentFile.filename() << "("
|
|
<< currentFile.currentSubimage() << ","
|
|
<< currentFile.currentMiplevel() << ")";
|
|
}
|
|
msg << " has different primaries than preceding images(s).";
|
|
if (options.assign_primaries == KHR_DF_PRIMARIES_UNSPECIFIED) {
|
|
msg << endl
|
|
<< "Use --assign_primaries (not recommended) to"
|
|
<< " stop this error.";
|
|
throw cant_create_image(msg.str());
|
|
} else
|
|
warning(msg.str());
|
|
// There is no convert_primaries option.
|
|
}
|
|
if (currentFormat.channelCount() != firstFormat.channelCount()) {
|
|
stringstream msg;
|
|
if (options.targetType == commandOptions::eUnspecified) {
|
|
msg << "Image";
|
|
} else{
|
|
msg << "Image in " << currentFile.filename() << "("
|
|
<< currentFile.currentSubimage() << ","
|
|
<< currentFile.currentMiplevel() << ")";
|
|
}
|
|
msg << " has a different component count than"
|
|
<< " preceding images(s).";
|
|
if (options.targetType == commandOptions::eUnspecified) {
|
|
msg << endl
|
|
<< "Use --target_type to stop this error (not recommended).";
|
|
throw cant_create_image(msg.str());
|
|
} else {
|
|
msg << endl
|
|
<< "The components of the level or layer derived "
|
|
<< "from this file will likely be significantly "
|
|
<< "different"
|
|
<< endl
|
|
<< "from those in other levels or layers.";
|
|
warning(msg.str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
toktxApp::setAstcMode(const targetImageSpec& target)
|
|
{
|
|
// If no astc mode option is specified and if input is <= 8bit
|
|
// default to LDR otherwise default to HDR
|
|
if (options.astcopts.mode == KTX_PACK_ASTC_ENCODER_MODE_DEFAULT) {
|
|
if (target.format().channelBitLength() <= 8)
|
|
options.astcopts.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
else
|
|
options.astcopts.mode = KTX_PACK_ASTC_ENCODER_MODE_HDR;
|
|
} else {
|
|
if (target.format().channelBitLength() > 8
|
|
&& options.astcopts.mode == KTX_PACK_ASTC_ENCODER_MODE_LDR)
|
|
{
|
|
// Input is > 8-bit and user wants LDR, issue quality loss warning.
|
|
stringstream msg;
|
|
msg << "Input file is 16-bit but ASTC LDR option is specified."
|
|
<< " Expect quality loss in the output."
|
|
<< endl;
|
|
warning(msg.str());
|
|
} else if (target.format().channelBitLength() < 16
|
|
&& options.astcopts.mode == KTX_PACK_ASTC_ENCODER_MODE_HDR)
|
|
{
|
|
// Input is < 8bit and user wants HDR, issue warning.
|
|
stringstream msg;
|
|
msg << "Input file is not 16-bit but HDR option is specified."
|
|
<< endl;
|
|
warning(msg.str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
toktxApp::validateOptions()
|
|
{
|
|
scApp::validateOptions();
|
|
|
|
if (options.automipmap + options.genmipmap + options.mipmap > 1) {
|
|
error("only one of --automipmap, --genmipmap and "
|
|
"--mipmap may be specified.");
|
|
usage();
|
|
exit(1);
|
|
}
|
|
if ((options.automipmap || options.genmipmap) && options.levels > 1) {
|
|
error("cannot specify --levels > 1 with --automipmap or --genmipmap.");
|
|
usage();
|
|
exit(1);
|
|
}
|
|
if (options.cubemap && options.lower_left_maps_to_s0t0) {
|
|
error("cubemaps require images to have an upper-left origin. "
|
|
"Ignoring --lower_left_maps_to_s0t0.");
|
|
options.lower_left_maps_to_s0t0 = 0;
|
|
}
|
|
if (options.cubemap && options.depth > 0) {
|
|
error("cubemaps cannot have 3D textures.");
|
|
usage();
|
|
exit(1);
|
|
}
|
|
if (options.layers && options.depth > 0) {
|
|
error("cannot have 3D array textures.");
|
|
usage();
|
|
exit(1);
|
|
}
|
|
if (options.scale != 1.0 && options.resize) {
|
|
error("only one of --scale and --resize can be specified.");
|
|
usage();
|
|
exit(1);
|
|
}
|
|
if (options.resize && options.mipmap) {
|
|
error("only one of --resize and --mipmap can be specified.");
|
|
usage();
|
|
exit(1);
|
|
}
|
|
|
|
if (options.depth > 1 && options.genmipmap) {
|
|
error("generation of mipmaps for 3d textures is not supported.\n"
|
|
"A PR to add this feature will be gratefully accepted!");
|
|
exit(1);
|
|
}
|
|
|
|
if (options.outfile.compare(_T("-")) != 0
|
|
&& options.outfile.find_last_of('.') == _tstring::npos)
|
|
{
|
|
options.outfile.append(options.ktx2 ? _T(".ktx2") : _T(".ktx"));
|
|
}
|
|
|
|
ktx_uint32_t requiredInputFiles = options.cubemap ? 6 : 1 * options.levels;
|
|
if (requiredInputFiles > options.infiles.size()) {
|
|
error("too few input files.");
|
|
exit(1);
|
|
}
|
|
/* Whether there are enough input files for all the mipmap levels in
|
|
* a full pyramid can only be checked when the first file has been
|
|
* read and the size determined.
|
|
*/
|
|
}
|
|
|
|
void
|
|
toktxApp::processEnvOptions() {
|
|
_tstring toktx_options;
|
|
_TCHAR* env_options = _tgetenv(_T("TOKTX_OPTIONS"));
|
|
|
|
if (env_options != nullptr)
|
|
toktx_options = env_options;
|
|
else
|
|
return;
|
|
|
|
if (!toktx_options.empty()) {
|
|
istringstream iss(toktx_options);
|
|
argvector arglist;
|
|
for (_tstring w; iss >> w; )
|
|
arglist.push_back(w);
|
|
|
|
argparser optparser(arglist, 0);
|
|
processOptions(optparser);
|
|
if (optparser.optind != arglist.size()) {
|
|
cerr << "Only options are allowed in the TOKTX_OPTIONS "
|
|
<< "environment variable." << endl;
|
|
usage();
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* @brief process a command line option
|
|
*
|
|
* @return
|
|
*
|
|
* @param[in] parser, an @c argparser holding the options to process.
|
|
*/
|
|
bool
|
|
toktxApp::processOption(argparser& parser, int opt)
|
|
{
|
|
// N.B. It is not possible for an optarg string to be a negative number
|
|
// because the leading '-' will make the parser think it is an option
|
|
// leading to a "missing required argument" error before this is ever called.
|
|
switch (opt) {
|
|
case 0:
|
|
break;
|
|
case 'a':
|
|
options.layers = (uint32_t)strtoi(parser.optarg.c_str());
|
|
if (options.layers == 0) {
|
|
cerr << name << ": "
|
|
<< "To create an array texture set --layers > 0." << endl;
|
|
exit(1);
|
|
}
|
|
break;
|
|
case 'd':
|
|
options.depth = (uint32_t)strtoi(parser.optarg.c_str());
|
|
if (options.depth == 0) {
|
|
cerr << name << ": "
|
|
<< "To create a 3d texture set --depth > 0." << endl;
|
|
exit(1);
|
|
}
|
|
break;
|
|
case 'l':
|
|
options.levels = (uint32_t)strtoi(parser.optarg.c_str());
|
|
if (options.levels < 2) {
|
|
cerr << name << ": "
|
|
<< "--levels must be > 1." << endl;
|
|
exit(1);
|
|
}
|
|
break;
|
|
case 'f':
|
|
options.gmopts.filter = parser.optarg;
|
|
break;
|
|
case 'F':
|
|
options.gmopts.filterScale = strtof(parser.optarg.c_str(), nullptr);
|
|
break;
|
|
case 'w':
|
|
if (!parser.optarg.compare("wrap")) {
|
|
options.gmopts.wrapMode
|
|
= basisu::Resampler::Boundary_Op::BOUNDARY_WRAP;
|
|
} else if (!parser.optarg.compare("clamp")) {
|
|
options.gmopts.wrapMode
|
|
= basisu::Resampler::Boundary_Op::BOUNDARY_CLAMP;
|
|
} else if (!parser.optarg.compare("reflect")) {
|
|
options.gmopts.wrapMode
|
|
= basisu::Resampler::Boundary_Op::BOUNDARY_REFLECT;
|
|
} else {
|
|
cerr << "Unrecognized mode \"" << parser.optarg
|
|
<< "\" passed to --wmode" << endl;
|
|
usage();
|
|
exit(1);
|
|
}
|
|
break;
|
|
case 'r':
|
|
{
|
|
istringstream iss(parser.optarg);
|
|
char x;
|
|
iss >> options.newGeom.width >> x >> options.newGeom.height;
|
|
if (iss.fail()) {
|
|
cerr << "Bad resize geometry." << endl;
|
|
usage();
|
|
exit(1);
|
|
}
|
|
options.resize = 1;
|
|
break;
|
|
}
|
|
case 's':
|
|
options.scale = strtof(parser.optarg.c_str(), nullptr);
|
|
if (options.scale > 2000.0f) {
|
|
cerr << name << ": Unreasonable scale factor of "
|
|
<< options.scale << "." << endl;
|
|
exit(1);
|
|
}
|
|
break;
|
|
case 1101:
|
|
validateSwizzle(parser.optarg);
|
|
options.swizzle = parser.optarg;
|
|
break;
|
|
case 1102:
|
|
for_each(parser.optarg.begin(), parser.optarg.end(), [](char & c) {
|
|
c = (char)::toupper(c);
|
|
});
|
|
if (parser.optarg.compare("R") == 0)
|
|
options.targetType = commandOptions::eR;
|
|
else if (parser.optarg.compare("RG") == 0)
|
|
options.targetType = commandOptions::eRG;
|
|
else if (parser.optarg.compare("RGB") == 0)
|
|
options.targetType = commandOptions::eRGB;
|
|
else if (parser.optarg.compare("RGBA") == 0)
|
|
options.targetType = commandOptions::eRGBA;
|
|
else {
|
|
cerr << name << ": unrecognized target_type \"" << parser.optarg
|
|
<< "\"." << endl;
|
|
usage();
|
|
exit(1);
|
|
}
|
|
break;
|
|
case 1103:
|
|
for_each(parser.optarg.begin(), parser.optarg.end(), [](char & c) {
|
|
c = (char)::tolower(c);
|
|
});
|
|
if (parser.optarg.compare("linear") == 0)
|
|
options.convert_oetf = KHR_DF_TRANSFER_LINEAR;
|
|
else if (parser.optarg.compare("srgb") == 0)
|
|
options.convert_oetf = KHR_DF_TRANSFER_SRGB;
|
|
break;
|
|
case 1104:
|
|
for_each(parser.optarg.begin(), parser.optarg.end(), [](char & c) {
|
|
c = (char)::tolower(c);
|
|
});
|
|
if (parser.optarg.compare("linear") == 0)
|
|
options.assign_oetf = KHR_DF_TRANSFER_LINEAR;
|
|
else if (parser.optarg.compare("srgb") == 0)
|
|
options.assign_oetf = KHR_DF_TRANSFER_SRGB;
|
|
break;
|
|
case 1105:
|
|
for_each(parser.optarg.begin(), parser.optarg.end(), [](char & c) {
|
|
c = (char)::tolower(c);
|
|
});
|
|
if (parser.optarg.compare("bt709") == 0)
|
|
options.assign_primaries = KHR_DF_PRIMARIES_BT709;
|
|
else if (parser.optarg.compare("none") == 0)
|
|
options.assign_primaries = KHR_DF_PRIMARIES_UNSPECIFIED;
|
|
if (parser.optarg.compare("srgb") == 0)
|
|
options.assign_primaries = KHR_DF_PRIMARIES_SRGB;
|
|
break;
|
|
case ':':
|
|
default:
|
|
return scApp::processOption(parser, opt);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void warning(const char *pFmt, va_list args) {
|
|
theApp.warning(pFmt, args);
|
|
}
|
|
|
|
void warning(const char *pFmt, ...) {
|
|
va_list args;
|
|
va_start(args, pFmt);
|
|
|
|
theApp.warning(pFmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void warning(const string& msg) {
|
|
theApp.warning(msg);
|
|
}
|
|
|
|
static ktx_uint32_t
|
|
log2(ktx_uint32_t v)
|
|
{
|
|
ktx_uint32_t e;
|
|
|
|
/* http://aggregate.org/MAGIC/ */
|
|
v |= (v >> 1);
|
|
v |= (v >> 2);
|
|
v |= (v >> 4);
|
|
v |= (v >> 8);
|
|
v |= (v >> 16);
|
|
v = v & ~(v >> 1);
|
|
|
|
e = (v & 0xAAAAAAAA) ? 1 : 0;
|
|
e |= (v & 0xCCCCCCCC) ? 2 : 0;
|
|
e |= (v & 0xF0F0F0F0) ? 4 : 0;
|
|
e |= (v & 0xFF00FF00) ? 8 : 0;
|
|
e |= (v & 0xFFFF0000) ? 16 : 0;
|
|
|
|
return e;
|
|
}
|
|
|
|
#if IMAGE_DEBUG
|
|
static void
|
|
dumpImage(_TCHAR* name, int width, int height, int components, int componentSize,
|
|
unsigned char* srcImage)
|
|
{
|
|
char formatstr[2048];
|
|
char *imagefmt;
|
|
char *fmtname;
|
|
int bitsPerComponent = componentSize == 2 ? 16 : 8;
|
|
|
|
switch (components) {
|
|
case 1:
|
|
imagefmt = "r b=";
|
|
fmtname = "R";
|
|
break;
|
|
case 2:
|
|
imagefmt = "rg b=";
|
|
fmtname = "RG";
|
|
break;
|
|
case 3:
|
|
imagefmt = "rgb b=";
|
|
fmtname = "RGB";
|
|
break;
|
|
case 4:
|
|
imagefmt = "rgba b=";
|
|
fmtname = "RGBA";
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
sprintf(formatstr, "%s%d w=%%d h=%%d t=\'%s %s%d\' %%p",
|
|
imagefmt,
|
|
bitsPerComponent,
|
|
name,
|
|
fmtname,
|
|
bitsPerComponent);
|
|
imdebug(formatstr, width, height, srcImage);
|
|
}
|
|
#endif
|