1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4
5 #include <cmath>
6 #include <cstdio>
7 #include <cstdlib>
8 #include <iostream>
9 #include <iterator>
10 #include <limits>
11 #include <sstream>
12
13 #include <OpenImageIO/argparse.h>
14 #include <OpenImageIO/color.h>
15 #include <OpenImageIO/dassert.h>
16 #include <OpenImageIO/filesystem.h>
17 #include <OpenImageIO/filter.h>
18 #include <OpenImageIO/imagebuf.h>
19 #include <OpenImageIO/imagebufalgo.h>
20 #include <OpenImageIO/imageio.h>
21 #include <OpenImageIO/strutil.h>
22 #include <OpenImageIO/sysutil.h>
23 #include <OpenImageIO/timer.h>
24
25 using namespace OIIO;
26
27
28 // # FIXME: Refactor all statics into a struct
29
30 // Basic runtime options
31 static std::string full_command_line;
32 static std::vector<std::string> filenames;
33 static std::string outputfilename;
34 static bool verbose = false;
35 static bool runstats = false;
36 static int nthreads = 0; // default: use #cores threads if available
37
38 // Conversion modes. If none are true, we just make an ordinary texture.
39 static bool mipmapmode = false;
40 static bool shadowmode = false;
41 static bool envlatlmode = false;
42 static bool envcubemode = false;
43 static bool lightprobemode = false;
44 static bool bumpslopesmode = false;
45
46
47 static std::string
filter_help_string()48 filter_help_string()
49 {
50 std::string s("Select filter for resizing (choices:");
51 for (int i = 0, e = Filter2D::num_filters(); i < e; ++i) {
52 FilterDesc d;
53 Filter2D::get_filterdesc(i, &d);
54 s.append(" ");
55 s.append(d.name);
56 }
57 s.append(", default=box)");
58 return s;
59 }
60
61
62
63 static std::string
colortitle_help_string()64 colortitle_help_string()
65 {
66 std::string s("Color Management Options ");
67 if (ColorConfig::supportsOpenColorIO()) {
68 s += "(OpenColorIO enabled)";
69 } else {
70 s += "(OpenColorIO DISABLED)";
71 }
72 return s;
73 }
74
75
76
77 static std::string
colorconvert_help_string()78 colorconvert_help_string()
79 {
80 std::string s
81 = "Apply a color space conversion to the image. "
82 "If the output color space is not the same bit depth "
83 "as input color space, it is your responsibility to set the data format "
84 "to the proper bit depth using the -d option. ";
85
86 s += " (choices: ";
87 ColorConfig colorconfig;
88 if (colorconfig.error() || colorconfig.getNumColorSpaces() == 0) {
89 s += "NONE";
90 } else {
91 for (int i = 0; i < colorconfig.getNumColorSpaces(); ++i) {
92 if (i != 0)
93 s += ", ";
94 s += colorconfig.getColorSpaceNameByIndex(i);
95 }
96 }
97 s += ")";
98 return s;
99 }
100
101
102
103 // Concatenate the command line into one string, optionally filtering out
104 // verbose attribute commands. Escape control chars in the arguments, and
105 // double-quote any that contain spaces.
106 static std::string
command_line_string(int argc,char * argv[],bool sansattrib)107 command_line_string(int argc, char* argv[], bool sansattrib)
108 {
109 std::string s;
110 for (int i = 0; i < argc; ++i) {
111 if (sansattrib) {
112 // skip any filtered attributes
113 if (!strcmp(argv[i], "--attrib") || !strcmp(argv[i], "-attrib")
114 || !strcmp(argv[i], "--sattrib")
115 || !strcmp(argv[i], "-sattrib")) {
116 i += 2; // also skip the following arguments
117 continue;
118 }
119 if (!strcmp(argv[i], "--sansattrib")
120 || !strcmp(argv[i], "-sansattrib")) {
121 continue;
122 }
123 }
124 std::string a = Strutil::escape_chars(argv[i]);
125 // If the string contains spaces
126 if (a.find(' ') != std::string::npos) {
127 // double quote args with spaces
128 s += '\"';
129 s += a;
130 s += '\"';
131 } else {
132 s += a;
133 }
134 if (i < argc - 1)
135 s += ' ';
136 }
137 return s;
138 }
139
140
141
142 static void
getargs(int argc,char * argv[],ImageSpec & configspec)143 getargs(int argc, char* argv[], ImageSpec& configspec)
144 {
145 // Basic runtime options
146 std::string dataformatname = "";
147 std::string fileformatname = "";
148 std::vector<std::string> mipimages;
149 int tile[3] = { 64, 64, 1 }; // FIXME if we ever support volume MIPmaps
150 std::string compression = "zip";
151 bool updatemode = false;
152 bool checknan = false;
153 std::string fixnan; // none, black, box3
154 bool set_full_to_pixels = false;
155 bool do_highlight_compensation = false;
156 std::string filtername;
157 // Options controlling file metadata or mipmap creation
158 float fovcot = 0.0f;
159 std::string wrap = "black";
160 std::string swrap;
161 std::string twrap;
162 bool doresize = false;
163 Imath::M44f Mcam(0.0f), Mscr(0.0f), MNDC(0.0f); // Initialize to 0
164 bool separate = false;
165 bool nomipmap = false;
166 bool prman_metadata = false;
167 bool constant_color_detect = false;
168 bool monochrome_detect = false;
169 bool opaque_detect = false;
170 bool compute_average = true;
171 int nchannels = -1;
172 bool prman = false;
173 bool oiio = false;
174 bool ignore_unassoc = false; // ignore unassociated alpha tags
175 bool unpremult = false;
176 bool sansattrib = false;
177 float sharpen = 0.0f;
178 std::string incolorspace;
179 std::string outcolorspace;
180 std::string colorconfigname;
181 std::string channelnames;
182 std::string bumpformat = "auto";
183 std::vector<std::string> string_attrib_names, string_attrib_values;
184 std::vector<std::string> any_attrib_names, any_attrib_values;
185 filenames.clear();
186
187 // clang-format off
188 ArgParse ap;
189 ap.intro("maketx -- convert images to tiled, MIP-mapped textures\n"
190 OIIO_INTRO_STRING);
191 ap.usage("maketx [options] file...");
192
193 ap.arg("filename")
194 .hidden()
195 .action([&](cspan<const char*> argv){ filenames.emplace_back(argv[0]); });
196 ap.arg("-v", &verbose)
197 .help("Verbose status messages");
198 ap.arg("-o %s:FILENAME", &outputfilename)
199 .help("Output filename");
200 ap.arg("--threads %d:NUMTHREADS", &nthreads)
201 .help("Number of threads (default: #cores)");
202 ap.arg("-u", &updatemode)
203 .help("Update mode");
204 ap.arg("--format %s:FILEFORMAT", &fileformatname)
205 .help("Specify output file format (default: guess from extension)");
206 ap.arg("--nchannels %d:N", &nchannels)
207 .help("Specify the number of output image channels.");
208 ap.arg("--chnames %s:CHANNELNAMES", &channelnames)
209 .help("Rename channels (comma-separated)");
210 ap.arg("-d %s:TYPE", &dataformatname)
211 .help("Set the output data format to one of: uint8, sint8, uint16, sint16, half, float");
212 ap.arg("--tile %d:WIDTH %d:HEIGHT", &tile[0], &tile[1])
213 .help("Specify tile size");
214 ap.arg("--separate", &separate)
215 .help("Use planarconfig separate (default: contiguous)");
216 ap.arg("--compression %s:NAME", &compression)
217 .help("Set the compression method (default = zip, if possible)");
218 ap.arg("--fovcot %f:FOVCAT", &fovcot)
219 .help("Override the frame aspect ratio. Default is width/height.");
220 ap.arg("--wrap %s:WRAP", &wrap)
221 .help("Specify wrap mode (black, clamp, periodic, mirror)");
222 ap.arg("--swrap %s:WRAP", &swrap)
223 .help("Specific s wrap mode separately");
224 ap.arg("--twrap %s:WRAP", &twrap)
225 .help("Specific t wrap mode separately");
226 ap.arg("--resize", &doresize)
227 .help("Resize textures to power of 2 (default: no)");
228 ap.arg("--noresize %!", &doresize)
229 .help("Do not resize textures to power of 2 (deprecated)");
230 ap.arg("--filter %s:FILTERNAME", &filtername)
231 .help(filter_help_string());
232 ap.arg("--hicomp", &do_highlight_compensation)
233 .help("Compress HDR range before resize, expand after.");
234 ap.arg("--sharpen %f:SHARPEN", &sharpen)
235 .help("Sharpen MIP levels (default = 0.0 = no)");
236 ap.arg("--nomipmap", &nomipmap)
237 .help("Do not make multiple MIP-map levels");
238 ap.arg("--checknan", &checknan)
239 .help("Check for NaN/Inf values (abort if found)");
240 ap.arg("--fixnan %s:STRATEGY", &fixnan)
241 .help("Attempt to fix NaN/Inf values in the image (options: none, black, box3)");
242 ap.arg("--fullpixels", &set_full_to_pixels)
243 .help("Set the 'full' image range to be the pixel data window");
244 ap.arg("--Mcamera %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f",
245 &Mcam[0][0], &Mcam[0][1], &Mcam[0][2], &Mcam[0][3],
246 &Mcam[1][0], &Mcam[1][1], &Mcam[1][2], &Mcam[1][3],
247 &Mcam[2][0], &Mcam[2][1], &Mcam[2][2], &Mcam[2][3],
248 &Mcam[3][0], &Mcam[3][1], &Mcam[3][2], &Mcam[3][3])
249 .help("Set the camera matrix");
250 ap.arg("--Mscreen %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f",
251 &Mscr[0][0], &Mscr[0][1], &Mscr[0][2], &Mscr[0][3],
252 &Mscr[1][0], &Mscr[1][1], &Mscr[1][2], &Mscr[1][3],
253 &Mscr[2][0], &Mscr[2][1], &Mscr[2][2], &Mscr[2][3],
254 &Mscr[3][0], &Mscr[3][1], &Mscr[3][2], &Mscr[3][3])
255 .help("Set the screen matrix");
256 ap.arg("--MNDC %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f",
257 &MNDC[0][0], &MNDC[0][1], &MNDC[0][2], &MNDC[0][3],
258 &MNDC[1][0], &MNDC[1][1], &MNDC[1][2], &MNDC[1][3],
259 &MNDC[2][0], &MNDC[2][1], &MNDC[2][2], &MNDC[2][3],
260 &MNDC[3][0], &MNDC[3][1], &MNDC[3][2], &MNDC[3][3])
261 .help("Set the NDC matrix");
262 ap.arg("--prman-metadata", &prman_metadata)
263 .help("Add prman specific metadata");
264 ap.arg("--attrib %L:NAME %L:VALUE", &any_attrib_names, &any_attrib_values)
265 .help("Sets metadata attribute (name, value)");
266 ap.arg("--sattrib %L:NAME %L:VALUE", &string_attrib_names, &string_attrib_values)
267 .help("Sets string metadata attribute (name, value)");
268 ap.arg("--sansattrib", &sansattrib)
269 .help("Write command line into Software & ImageHistory but remove --sattrib and --attrib options");
270 ap.arg("--constant-color-detect", &constant_color_detect)
271 .help("Create 1-tile textures from constant color inputs");
272 ap.arg("--monochrome-detect", &monochrome_detect)
273 .help("Create 1-channel textures from monochrome inputs");
274 ap.arg("--opaque-detect", &opaque_detect)
275 .help("Drop alpha channel that is always 1.0");
276 ap.arg("--no-compute-average %!", &compute_average)
277 .help("Don't compute and store average color");
278 ap.arg("--ignore-unassoc", &ignore_unassoc)
279 .help("Ignore unassociated alpha tags in input (don't autoconvert)");
280 ap.arg("--runstats", &runstats)
281 .help("Print runtime statistics");
282 ap.arg("--stats", &runstats)
283 .hidden(); // DEPRECATED 1.6
284 ap.arg("--mipimage %L:FILENAME", &mipimages)
285 .help("Specify an individual MIP level");
286
287 ap.separator("Basic modes (default is plain texture):");
288 ap.arg("--shadow", &shadowmode)
289 .help("Create shadow map");
290 ap.arg("--envlatl", &envlatlmode)
291 .help("Create lat/long environment map");
292 ap.arg("--lightprobe", &lightprobemode)
293 .help("Create lat/long environment map from a light probe");
294 ap.arg("--bumpslopes", &bumpslopesmode)
295 .help("Create a 6 channels bump-map with height, derivatives and square derivatives from an height or a normal map");
296 ap.arg("--bumpformat %s:NAME", &bumpformat)
297 .help("Specify the interpretation of a 3-channel input image for --bumpslopes: \"height\", \"normal\" or \"auto\" (default).");
298 // "--envcube", &envcubemode, "Create cubic env map (file order: px, nx, py, ny, pz, nz) (UNIMP)",
299
300 ap.separator(colortitle_help_string());
301 ap.arg("--colorconfig %s:FILENAME", &colorconfigname)
302 .help("Explicitly specify an OCIO configuration file");
303 ap.arg("--colorconvert %s:IN %s:OUT", &incolorspace, &outcolorspace)
304 .help(colorconvert_help_string());
305 ap.arg("--unpremult", &unpremult)
306 .help("Unpremultiply before color conversion, then premultiply "
307 "after the color conversion. You'll probably want to use this flag "
308 "if your image contains an alpha channel.");
309
310 ap.separator("Configuration Presets");
311 ap.arg("--prman", &prman)
312 .help("Use PRMan-safe settings for tile size, planarconfig, and metadata.");
313 ap.arg("--oiio", &oiio)
314 .help("Use OIIO-optimized settings for tile size, planarconfig, metadata.");
315
316 // clang-format on
317 ap.parse(argc, (const char**)argv);
318 if (filenames.empty()) {
319 ap.briefusage();
320 std::cout << "\nFor detailed help: maketx --help\n";
321 exit(EXIT_SUCCESS);
322 }
323
324 int optionsum = ((int)shadowmode + (int)envlatlmode + (int)envcubemode
325 + (int)lightprobemode)
326 + (int)bumpslopesmode;
327 if (optionsum > 1) {
328 std::cerr
329 << "maketx ERROR: At most one of the following options may be set:\n"
330 << "\t--shadow --envlatl --envcube --lightprobe\n";
331 exit(EXIT_FAILURE);
332 }
333 if (optionsum == 0)
334 mipmapmode = true;
335
336 if (prman && oiio) {
337 std::cerr
338 << "maketx ERROR: '--prman' compatibility, and '--oiio' optimizations are mutually exclusive.\n";
339 std::cerr
340 << "\tIf you'd like both prman and oiio compatibility, you should choose --prman\n";
341 std::cerr << "\t(at the expense of oiio-specific optimizations)\n";
342 exit(EXIT_FAILURE);
343 }
344
345 if (filenames.size() != 1) {
346 std::cerr << "maketx ERROR: requires exactly one input filename\n";
347 exit(EXIT_FAILURE);
348 }
349
350
351 // std::cout << "Converting " << filenames[0] << " to " << outputfilename << "\n";
352
353 // Figure out which data format we want for output
354 if (!dataformatname.empty()) {
355 if (dataformatname == "uint8")
356 configspec.format = TypeDesc::UINT8;
357 else if (dataformatname == "int8" || dataformatname == "sint8")
358 configspec.format = TypeDesc::INT8;
359 else if (dataformatname == "uint16")
360 configspec.format = TypeDesc::UINT16;
361 else if (dataformatname == "int16" || dataformatname == "sint16")
362 configspec.format = TypeDesc::INT16;
363 else if (dataformatname == "half")
364 configspec.format = TypeDesc::HALF;
365 else if (dataformatname == "float")
366 configspec.format = TypeDesc::FLOAT;
367 else if (dataformatname == "double")
368 configspec.format = TypeDesc::DOUBLE;
369 else {
370 std::cerr << "maketx ERROR: unknown data format \""
371 << dataformatname << "\"\n";
372 exit(EXIT_FAILURE);
373 }
374 }
375
376 configspec.tile_width = tile[0];
377 configspec.tile_height = tile[1];
378 configspec.tile_depth = tile[2];
379 configspec.attribute("compression", compression);
380 if (fovcot != 0.0f)
381 configspec.attribute("fovcot", fovcot);
382 configspec.attribute("planarconfig", separate ? "separate" : "contig");
383 if (Mcam != Imath::M44f(0.0f))
384 configspec.attribute("worldtocamera", TypeMatrix, &Mcam);
385 if (Mscr != Imath::M44f(0.0f))
386 configspec.attribute("worldtoscreen", TypeMatrix, &Mscr);
387 if (MNDC != Imath::M44f(0.0f))
388 configspec.attribute("worldtoNDC", TypeMatrix, &MNDC);
389 std::string wrapmodes = (swrap.size() ? swrap : wrap) + ','
390 + (twrap.size() ? twrap : wrap);
391 configspec.attribute("wrapmodes", wrapmodes);
392
393 configspec.attribute("maketx:verbose", verbose);
394 configspec.attribute("maketx:runstats", runstats);
395 configspec.attribute("maketx:resize", doresize);
396 configspec.attribute("maketx:nomipmap", nomipmap);
397 configspec.attribute("maketx:updatemode", updatemode);
398 configspec.attribute("maketx:constant_color_detect", constant_color_detect);
399 configspec.attribute("maketx:monochrome_detect", monochrome_detect);
400 configspec.attribute("maketx:opaque_detect", opaque_detect);
401 configspec.attribute("maketx:compute_average", compute_average);
402 configspec.attribute("maketx:unpremult", unpremult);
403 configspec.attribute("maketx:incolorspace", incolorspace);
404 configspec.attribute("maketx:outcolorspace", outcolorspace);
405 configspec.attribute("maketx:colorconfig", colorconfigname);
406 configspec.attribute("maketx:checknan", checknan);
407 configspec.attribute("maketx:fixnan", fixnan);
408 configspec.attribute("maketx:set_full_to_pixels", set_full_to_pixels);
409 configspec.attribute("maketx:highlightcomp",
410 (int)do_highlight_compensation);
411 configspec.attribute("maketx:sharpen", sharpen);
412 if (filtername.size())
413 configspec.attribute("maketx:filtername", filtername);
414 configspec.attribute("maketx:nchannels", nchannels);
415 configspec.attribute("maketx:channelnames", channelnames);
416 if (fileformatname.size())
417 configspec.attribute("maketx:fileformatname", fileformatname);
418 configspec.attribute("maketx:prman_metadata", prman_metadata);
419 configspec.attribute("maketx:oiio_options", oiio);
420 configspec.attribute("maketx:prman_options", prman);
421 if (mipimages.size())
422 configspec.attribute("maketx:mipimages", Strutil::join(mipimages, ";"));
423 if (bumpslopesmode)
424 configspec.attribute("maketx:bumpformat", bumpformat);
425
426 std::string cmdline
427 = Strutil::sprintf("OpenImageIO %s : %s", OIIO_VERSION_STRING,
428 command_line_string(argc, argv, sansattrib));
429 configspec.attribute("Software", cmdline);
430 configspec.attribute("maketx:full_command_line", cmdline);
431
432 // Add user-specified string attributes
433 for (size_t i = 0; i < string_attrib_names.size(); ++i) {
434 configspec.attribute(string_attrib_names[i], string_attrib_values[i]);
435 }
436
437 // Add user-specified "any" attributes -- try to deduce the type
438 for (size_t i = 0; i < any_attrib_names.size(); ++i) {
439 string_view s = any_attrib_values[i];
440 // Does it parse as an int (and nothing more?)
441 int ival;
442 if (Strutil::parse_int(s, ival)) {
443 Strutil::skip_whitespace(s);
444 if (!s.size()) {
445 configspec.attribute(any_attrib_names[i], ival);
446 continue;
447 }
448 }
449 s = any_attrib_values[i];
450 // Does it parse as a float (and nothing more?)
451 float fval;
452 if (Strutil::parse_float(s, fval)) {
453 Strutil::skip_whitespace(s);
454 if (!s.size()) {
455 configspec.attribute(any_attrib_names[i], fval);
456 continue;
457 }
458 }
459 // OK, treat it like a string
460 configspec.attribute(any_attrib_names[i], any_attrib_values[i]);
461 }
462
463 if (ignore_unassoc) {
464 configspec.attribute("maketx:ignore_unassoc", (int)ignore_unassoc);
465 ImageCache* ic = ImageCache::create(); // get the shared one
466 ic->attribute("unassociatedalpha", (int)ignore_unassoc);
467 }
468 }
469
470
471
472 int
main(int argc,char * argv[])473 main(int argc, char* argv[])
474 {
475 Timer alltimer;
476
477 // Helpful for debugging to make sure that any crashes dump a stack
478 // trace.
479 Sysutil::setup_crash_stacktrace("stdout");
480
481 // Globally force classic "C" locale, and turn off all formatting
482 // internationalization, for the entire maketx application.
483 std::locale::global(std::locale::classic());
484
485 ImageSpec configspec;
486 Filesystem::convert_native_arguments(argc, (const char**)argv);
487 getargs(argc, argv, configspec);
488
489 OIIO::attribute("threads", nthreads);
490
491 // N.B. This will apply to the default IC that any ImageBuf's get.
492 ImageCache* ic = ImageCache::create(); // get the shared one
493 ic->attribute("forcefloat", 1); // Force float upon read
494 ic->attribute("max_memory_MB", 1024.0); // 1 GB cache
495
496 ImageBufAlgo::MakeTextureMode mode = ImageBufAlgo::MakeTxTexture;
497 if (shadowmode)
498 mode = ImageBufAlgo::MakeTxShadow;
499 if (envlatlmode)
500 mode = ImageBufAlgo::MakeTxEnvLatl;
501 if (lightprobemode)
502 mode = ImageBufAlgo::MakeTxEnvLatlFromLightProbe;
503 if (bumpslopesmode)
504 mode = ImageBufAlgo::MakeTxBumpWithSlopes;
505
506 bool ok = ImageBufAlgo::make_texture(mode, filenames[0], outputfilename,
507 configspec, &std::cout);
508 if (runstats)
509 std::cout << "\n" << ic->getstats();
510
511 return ok ? 0 : EXIT_FAILURE;
512 }
513