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