From cc9159d3876943644afea0e6749f4c96657bfbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 27 Sep 2022 17:11:04 +0200 Subject: [PATCH] {scene,image}converter: add --info options for plugins. Lists features, aliases as well as documented contents of the whole configuration file. Useful to not need to look up online docs when working on the command line. --- doc/changelog.dox | 6 + .../Implementation/sceneConverterUtilities.h | 6 + src/Magnum/SceneTools/Test/CMakeLists.txt | 15 + .../Test/SceneConverterImplementationTest.cpp | 53 ++- .../SceneTools/Test/SceneConverterTest.cpp | 38 ++- .../info-converter.txt | 6 + .../info-image-converter.txt | 11 + .../info-importer-ignored-input-output.txt | 5 + .../SceneConverterTestFiles/info-importer.txt | 5 + src/Magnum/SceneTools/Test/configure.h.cmake | 1 + src/Magnum/SceneTools/sceneconverter.cpp | 116 +++++-- .../Trade/Implementation/converterUtilities.h | 103 ++++++ src/Magnum/Trade/Test/CMakeLists.txt | 38 +++ .../Test/ImageConverterImplementationTest.cpp | 315 +++++++++++++++++- src/Magnum/Trade/Test/ImageConverterTest.cpp | 27 +- .../info-converter.txt | 11 + .../info-importer-ignored-input-output.txt | 6 + .../ImageConverterTestFiles/info-importer.txt | 6 + src/Magnum/Trade/Test/configure.h.cmake | 7 + src/Magnum/Trade/imageconverter.cpp | 95 +++++- 20 files changed, 826 insertions(+), 44 deletions(-) create mode 100644 src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-converter.txt create mode 100644 src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-image-converter.txt create mode 100644 src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer-ignored-input-output.txt create mode 100644 src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer.txt create mode 100644 src/Magnum/Trade/Test/ImageConverterTestFiles/info-converter.txt create mode 100644 src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer-ignored-input-output.txt create mode 100644 src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer.txt diff --git a/doc/changelog.dox b/doc/changelog.dox index 2977d1c51..8d0fb2062 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -229,6 +229,9 @@ See also: correctly handle all corner cases yet and may assert on certain inputs. - The @ref magnum-sceneconverter "magnum-sceneconverter" `--info` output is now more compact and colored for better readability +- Added `--info-importer`, `--info-converter` and `--info-image-converter` + options to @ref magnum-sceneconverter "magnum-sceneconverter", listing + plugin features and configuration file contents @subsubsection changelog-latest-new-shaders Shaders library @@ -321,6 +324,9 @@ See also: converters together - The @ref magnum-imageconverter "magnum-imageconverter" `--info` output is now more compact and colored for better readability +- Added `--info-importer` and `--info-converter` options to + @ref magnum-imageconverter "magnum-imageconverter", listing plugin features + and configuration file contents @subsubsection changelog-latest-new-vk Vk library diff --git a/src/Magnum/SceneTools/Implementation/sceneConverterUtilities.h b/src/Magnum/SceneTools/Implementation/sceneConverterUtilities.h index 24c827365..330c23b11 100644 --- a/src/Magnum/SceneTools/Implementation/sceneConverterUtilities.h +++ b/src/Magnum/SceneTools/Implementation/sceneConverterUtilities.h @@ -33,6 +33,7 @@ #include #include "Magnum/Math/FunctionsBatch.h" +#include "Magnum/Trade/AbstractSceneConverter.h" #include "Magnum/Trade/AnimationData.h" #include "Magnum/Trade/CameraData.h" #include "Magnum/Trade/LightData.h" @@ -52,6 +53,11 @@ using namespace Containers::Literals; particular magnum-sceneconverter and its tests */ namespace { +void printSceneConverterInfo(const Debug::Flags useColor, const Trade::AbstractSceneConverter& converter) { + Trade::Implementation::printPluginInfo(useColor, converter); + Trade::Implementation::printPluginConfigurationInfo(useColor, converter); +} + /** @todo const Array& doesn't work, minmax() would fail to match */ template Containers::String calculateBounds(Containers::Array&& attribute) { /** @todo clean up when Debug::toString() exists */ diff --git a/src/Magnum/SceneTools/Test/CMakeLists.txt b/src/Magnum/SceneTools/Test/CMakeLists.txt index 208885c27..fa9b3a283 100644 --- a/src/Magnum/SceneTools/Test/CMakeLists.txt +++ b/src/Magnum/SceneTools/Test/CMakeLists.txt @@ -40,6 +40,9 @@ endif() if(MAGNUM_WITH_SCENECONVERTER AND CORRADE_TARGET_UNIX) set(SCENECONVERTER_EXECUTABLE_FILENAME $) endif() +if(MAGNUM_WITH_ANYSCENECONVERTER AND NOT MAGNUM_BUILD_PLUGINS_STATIC AND NOT MAGNUM_ANYSCENECONVERTER_BUILD_STATIC) + set(ANYSCENECONVERTER_PLUGIN_FILENAME $) +endif() # First replace ${} variables, then $<> generator expressions configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake @@ -69,6 +72,14 @@ corrade_add_test(SceneToolsSceneConverterImple___Test SceneConverterImplementati SceneConverterImplementationTestFiles/info-skins.txt SceneConverterImplementationTestFiles/info-textures.txt) target_include_directories(SceneToolsSceneConverterImple___Test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +if(MAGNUM_WITH_ANYSCENECONVERTER) + if(MAGNUM_BUILD_PLUGINS_STATIC OR MAGNUM_ANYSCENECONVERTER_BUILD_STATIC) + target_link_libraries(SceneToolsSceneConverterImple___Test PRIVATE AnySceneConverter) + else() + # So the plugins get properly built when building the test + add_dependencies(SceneToolsSceneConverterImple___Test AnySceneConverter) + endif() +endif() # Executable testing is implemented on Unix platforms only at the moment if(CORRADE_TARGET_UNIX AND NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) @@ -97,6 +108,10 @@ if(CORRADE_TARGET_UNIX AND NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) SceneConverterTestFiles/images-3d-1x1x1.gltf SceneConverterTestFiles/info-data.txt SceneConverterTestFiles/info-data-ignored-output.txt + SceneConverterTestFiles/info-converter.txt + SceneConverterTestFiles/info-image-converter.txt + SceneConverterTestFiles/info-importer.txt + SceneConverterTestFiles/info-importer-ignored-input-output.txt SceneConverterTestFiles/point.obj SceneConverterTestFiles/quad-duplicates-fuzzy.obj SceneConverterTestFiles/quad-duplicates.obj diff --git a/src/Magnum/SceneTools/Test/SceneConverterImplementationTest.cpp b/src/Magnum/SceneTools/Test/SceneConverterImplementationTest.cpp index cbc2b36c5..c529187ac 100644 --- a/src/Magnum/SceneTools/Test/SceneConverterImplementationTest.cpp +++ b/src/Magnum/SceneTools/Test/SceneConverterImplementationTest.cpp @@ -44,6 +44,11 @@ namespace Magnum { namespace SceneTools { namespace Test { namespace { struct SceneConverterImplementationTest: TestSuite::Tester { explicit SceneConverterImplementationTest(); + /* printPluginInfo(), printPluginConfigurationInfo() and + printImporterInfo() tested thoroughly in + Trade/Test/ImageConverterImplementationTest.cpp already */ + void converterInfo(); + void infoEmpty(); void infoScenesObjects(); void infoAnimations(); @@ -60,6 +65,9 @@ struct SceneConverterImplementationTest: TestSuite::Tester { void infoError(); Utility::Arguments _infoArgs; + + /* Explicitly forbid system-wide plugin dependencies */ + PluginManager::Manager _converterManager{"nonexistent"}; }; using namespace Containers::Literals; @@ -86,7 +94,9 @@ const struct { }; SceneConverterImplementationTest::SceneConverterImplementationTest() { - addTests({&SceneConverterImplementationTest::infoEmpty}); + addTests({&SceneConverterImplementationTest::converterInfo, + + &SceneConverterImplementationTest::infoEmpty}); addInstancedTests({&SceneConverterImplementationTest::infoScenesObjects}, Containers::arraySize(InfoScenesObjectsData)); @@ -121,6 +131,47 @@ SceneConverterImplementationTest::SceneConverterImplementationTest() { .addBooleanOption("info-textures") .addBooleanOption("info-images") .addBooleanOption("bounds"); + + /* Load the plugin directly from the build tree. Otherwise it's static and + already loaded. */ + #ifdef ANYSCENECONVERTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_converterManager.load(ANYSCENECONVERTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + + /* To avoid warnings that printImageConverterInfo() / printImporterInfo() + is unused. Again, those are tested in ImageConverterImplementationTest + already. */ + static_cast(Trade::Implementation::printImageConverterInfo); + static_cast(Trade::Implementation::printImporterInfo); +} + +void SceneConverterImplementationTest::converterInfo() { + /* Check if the required plugin can be loaded. Catches also ABI and + interface mismatch errors. */ + if(!(_converterManager.load("AnySceneConverter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("AnySceneConverter plugin can't be loaded."); + + Containers::Pointer converter = _converterManager.instantiate("AnySceneConverter"); + /** @todo pick a plugin that has some actual configuration */ + converter->configuration().setValue("something", "is there"); + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printSceneConverterInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, *converter); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printSceneConverterInfo(Debug::Flag::DisableColors, *converter); + CORRADE_COMPARE(out.str(), + "Plugin name: AnySceneConverter\n" + "Features:\n" + " ConvertMeshToFile\n" + " ConvertMultipleToFile\n" + "Configuration:\n" + " something=is there\n"); } void SceneConverterImplementationTest::infoEmpty() { diff --git a/src/Magnum/SceneTools/Test/SceneConverterTest.cpp b/src/Magnum/SceneTools/Test/SceneConverterTest.cpp index 676dca78e..3448621d1 100644 --- a/src/Magnum/SceneTools/Test/SceneConverterTest.cpp +++ b/src/Magnum/SceneTools/Test/SceneConverterTest.cpp @@ -57,21 +57,47 @@ const struct { const char* name; Containers::Array args; const char* requiresImporter; + const char* requiresConverter; + const char* requiresImageConverter; const char* expected; } InfoData[]{ + {"importer", Containers::array({ + "--info-importer", "-i", "someOption=yes"}), + "AnySceneImporter", nullptr, nullptr, + "info-importer.txt"}, + {"converter", Containers::array({ + "-C", "AnySceneConverter", "--info-converter", "-c", "someOption=yes"}), + nullptr, "AnySceneConverter", nullptr, + "info-converter.txt"}, + {"converter, implicit", Containers::array({ + "--info-converter", "-c", "someOption=yes"}), + nullptr, "AnySceneConverter", nullptr, + "info-converter.txt"}, + {"image converter", Containers::array({ + "-P", "AnyImageConverter", "--info-image-converter", "-p", "someOption=yes"}), + nullptr, nullptr, "AnyImageConverter", + "info-image-converter.txt"}, + {"image converter, implicit", Containers::array({ + "--info-image-converter", "-p", "someOption=yes"}), + nullptr, nullptr, "AnyImageConverter", + "info-image-converter.txt"}, + {"importer, ignored input and output", Containers::array({ + "--info-importer", "input.obj", "output.ply"}), + "AnySceneImporter", nullptr, nullptr, + "info-importer-ignored-input-output.txt"}, {"data", Containers::array({ "-I", "ObjImporter", "--info", Utility::Path::join(SCENETOOLS_TEST_DIR, "SceneConverterTestFiles/point.obj")}), - "ObjImporter", + "ObjImporter", nullptr, nullptr, "info-data.txt"}, {"data, map", Containers::array({ "--map", "-I", "ObjImporter", "--info", Utility::Path::join(SCENETOOLS_TEST_DIR, "SceneConverterTestFiles/point.obj")}), - "ObjImporter", + "ObjImporter", nullptr, nullptr, /** @todo change to something else once we have a plugin that can zero-copy pass the imported data */ "info-data.txt"}, {"data, ignored output file", Containers::array({ "-I", "ObjImporter", "--info", Utility::Path::join(SCENETOOLS_TEST_DIR, "SceneConverterTestFiles/point.obj"), "whatever.ply"}), - "ObjImporter", + "ObjImporter", nullptr, nullptr, "info-data-ignored-output.txt"} }; @@ -783,8 +809,14 @@ void SceneConverterTest::info() { /* Check if required plugins can be loaded. Catches also ABI and interface mismatch errors. */ PluginManager::Manager importerManager{MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR}; + PluginManager::Manager imageConverterManager{MAGNUM_PLUGINS_IMAGECONVERTER_INSTALL_DIR}; + PluginManager::Manager converterManager{MAGNUM_PLUGINS_SCENECONVERTER_INSTALL_DIR}; if(data.requiresImporter && !(importerManager.load(data.requiresImporter) & PluginManager::LoadState::Loaded)) CORRADE_SKIP(data.requiresImporter << "plugin can't be loaded."); + if(data.requiresConverter && !(converterManager.load(data.requiresConverter) & PluginManager::LoadState::Loaded)) + CORRADE_SKIP(data.requiresConverter << "plugin can't be loaded."); + if(data.requiresImageConverter && !(imageConverterManager.load(data.requiresImageConverter) & PluginManager::LoadState::Loaded)) + CORRADE_SKIP(data.requiresImageConverter << "plugin can't be loaded."); CORRADE_VERIFY(true); /* capture correct function name */ diff --git a/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-converter.txt b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-converter.txt new file mode 100644 index 000000000..813a4b257 --- /dev/null +++ b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-converter.txt @@ -0,0 +1,6 @@ +Plugin name: AnySceneConverter +Features: + ConvertMeshToFile + ConvertMultipleToFile +Configuration: + someOption=yes diff --git a/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-image-converter.txt b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-image-converter.txt new file mode 100644 index 000000000..3ab9b48e2 --- /dev/null +++ b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-image-converter.txt @@ -0,0 +1,11 @@ +Plugin name: AnyImageConverter +Features: + Convert1DToFile + Convert2DToFile + Convert3DToFile + ConvertCompressed1DToFile + ConvertCompressed2DToFile + ConvertCompressed3DToFile + Levels +Configuration: + someOption=yes diff --git a/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer-ignored-input-output.txt b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer-ignored-input-output.txt new file mode 100644 index 000000000..ea9e95c20 --- /dev/null +++ b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer-ignored-input-output.txt @@ -0,0 +1,5 @@ +Ignoring input file for --info: input.obj +Ignoring output file for --info: output.ply +Plugin name: AnySceneImporter +Features: + FileCallback diff --git a/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer.txt b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer.txt new file mode 100644 index 000000000..ebbddd1bb --- /dev/null +++ b/src/Magnum/SceneTools/Test/SceneConverterTestFiles/info-importer.txt @@ -0,0 +1,5 @@ +Plugin name: AnySceneImporter +Features: + FileCallback +Configuration: + someOption=yes diff --git a/src/Magnum/SceneTools/Test/configure.h.cmake b/src/Magnum/SceneTools/Test/configure.h.cmake index 76936f87d..b2cd664c5 100644 --- a/src/Magnum/SceneTools/Test/configure.h.cmake +++ b/src/Magnum/SceneTools/Test/configure.h.cmake @@ -26,6 +26,7 @@ #define SCENETOOLS_TEST_DIR "${SCENETOOLS_TEST_DIR}" #define SCENETOOLS_TEST_OUTPUT_DIR "${SCENETOOLS_TEST_OUTPUT_DIR}" #cmakedefine SCENECONVERTER_EXECUTABLE_FILENAME "${SCENECONVERTER_EXECUTABLE_FILENAME}" +#cmakedefine ANYSCENECONVERTER_PLUGIN_FILENAME "${ANYSCENECONVERTER_PLUGIN_FILENAME}" #ifdef CORRADE_TARGET_WINDOWS #ifdef CORRADE_IS_DEBUG_BUILD diff --git a/src/Magnum/SceneTools/sceneconverter.cpp b/src/Magnum/SceneTools/sceneconverter.cpp index cb67576e4..d585f42ed 100644 --- a/src/Magnum/SceneTools/sceneconverter.cpp +++ b/src/Magnum/SceneTools/sceneconverter.cpp @@ -126,7 +126,8 @@ magnum-sceneconverter [-h|--help] [-I|--importer PLUGIN] [-c|--converter-options key=val,key2=val2,…]... [-p|--image-converter-options key=val,key2=val2,…]... [-m|--mesh-converter-options key=val,key2=val2,…]... - [--mesh ID] [--mesh-level INDEX] [--concatenate-meshes] [--info-animations] + [--mesh ID] [--mesh-level INDEX] [--concatenate-meshes] [--info-importer] + [--info-converter] [--info-image-converter] [--info-animations] [--info-images] [--info-lights] [--info-cameras] [--info-materials] [--info-meshes] [--info-objects] [--info-scenes] [--info-skins] [--info-textures] [--info] [--color on|4bit|off|auto] [--bounds] @@ -169,6 +170,11 @@ Arguments: - `--mesh-level LEVEL` --- level to select for single-mesh conversion - `--concatenate-meshes` -- flatten mesh hierarchy and concatenate them all together @m_class{m-label m-warning} **experimental** +- `--info-importer` --- print info about the importer plugin and exit +- `--info-converter` --- print info about the scene or mesh converter plugin + and exit +- `--info-image-converter` --- print info about the image converter plugin + and exit - `--info-animations` --- print into about animations in the input file and exit - `--info-images` --- print into about images in the input file and exit @@ -182,18 +188,25 @@ Arguments: - `--info-skins` --- print into about skins in the input file and exit - `--info-textures` --- print into about textures in the input file and exit - `--info` --- print info about everything in the input file and exit, same - as specifying all other `--info-*` options together + as specifying all other data-related `--info-*` options together - `--color` --- colored output for `--info` (default: `auto`) - `--bounds` --- show bounds of known attributes in `--info` output - `-v`, `--verbose` --- verbose output from importer and converter plugins - `--profile` --- measure import and conversion time -If any of the `--info-*` options are given, the utility will print information -about given data present in the file. In this case no conversion is done and -output file doesn't need to be specified. In case one data references another -and both `--info-*` options are specified, the output will also list reference -count (for example, `--info-scenes` together with `--info-meshes` will print -how many objects reference given mesh). +If any of the `--info-importer`, `--info-converter` or `--info-image-converter` +options are given, the utility will print information about given plugin +specified via the `-I`, `-C` or `-P` option, including its configuration +options potentially overriden with `-i`, `-c` or `-p`. In this case no file is +read and no conversion is done and neither the input nor the output file needs +to be specified. + +If any of the other `--info-*` options are given, the utility will print +information about given data. In this case the input file is read but no +conversion is done and the output file doesn't need to be specified. In case +one data references another and both `--info-*` options are specified, the +output will also list reference count (for example, `--info-scenes` together +with `--info-meshes` will print how many objects reference given mesh). The `-i`, `-c` and `-m` arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the `=` @@ -239,7 +252,13 @@ using namespace Containers::Literals; namespace { -bool isInfoRequested(const Utility::Arguments& args) { +bool isPluginInfoRequested(const Utility::Arguments& args) { + return args.isSet("info-importer") || + args.isSet("info-converter") || + args.isSet("info-image-converter"); +} + +bool isDataInfoRequested(const Utility::Arguments& args) { return args.isSet("info-animations") || args.isSet("info-images") || args.isSet("info-lights") || @@ -333,6 +352,9 @@ int main(int argc, char** argv) { .addOption("mesh").setHelp("mesh", "convert just a single mesh instead of the whole scene, ignored if --concatenate-meshes is specified", "ID") .addOption("mesh-level").setHelp("mesh-level", "level to select for single-mesh conversion", "index") .addBooleanOption("concatenate-meshes").setHelp("concatenate-meshes", "flatten mesh hierarchy and concatenate them all together") + .addBooleanOption("info-importer").setHelp("info-importer", "print info about the importer plugin and exit") + .addBooleanOption("info-converter").setHelp("info-converter", "print info about the scene or mesh converter plugin and exit") + .addBooleanOption("info-image-converter").setHelp("info-image-converter", "print info about the image converter plugin and exit") .addBooleanOption("info-animations").setHelp("info-animations", "print info about animations in the input file and exit") .addBooleanOption("info-images").setHelp("info-images", "print info about images in the input file and exit") .addBooleanOption("info-lights").setHelp("info-lights", "print info about images in the input file and exit") @@ -343,27 +365,39 @@ int main(int argc, char** argv) { .addBooleanOption("info-scenes").setHelp("info-scenes", "print info about scenes in the input file and exit") .addBooleanOption("info-skins").setHelp("info-skins", "print info about skins in the input file and exit") .addBooleanOption("info-textures").setHelp("info-textures", "print info about textures in the input file and exit") - .addBooleanOption("info").setHelp("info", "print info about everything in the input file and exit, same as specifying all other --info-* options together") + .addBooleanOption("info").setHelp("info", "print info about everything in the input file and exit, same as specifying all other data-related --info-* options together") .addOption("color", "auto").setHelp("color", "colored output for --info", "on|4bit|off|auto") .addBooleanOption("bounds").setHelp("bounds", "show bounds of known attributes in --info output") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins") .addBooleanOption("profile").setHelp("profile", "measure import and conversion time") .setParseErrorCallback([](const Utility::Arguments& args, Utility::Arguments::ParseError error, const std::string& key) { - /* If --info is passed, we don't need the output argument */ + /* If --info for plugins is passed, we don't need the input */ + if(error == Utility::Arguments::ParseError::MissingArgument && + key == "input" && isPluginInfoRequested(args)) + return true; + /* If --info for plugins or data is passed, we don't need the + output argument */ if(error == Utility::Arguments::ParseError::MissingArgument && - key == "output" && isInfoRequested(args)) return true; + key == "output" && (isPluginInfoRequested(args) || isDataInfoRequested(args))) + return true; /* Handle all other errors as usual */ return false; }) .setGlobalHelp(R"(Converts scenes of different formats. -If any of the --info-* options are given, the utility will print information -about given data present in the file. In this case no conversion is done and -output file doesn't need to be specified. In case one data references another -and both --info-* options are specified, the output will also list reference -count (for example, --info-scenes together with --info-meshes will print how -many objects reference given mesh). +If any of the --info-importer, --info-converter or --info-image-converter +options are given, the utility will print information about given plugin +specified via the -I, -C or -P option. In this case no file is read and no +conversion is done and neither the input nor the output file needs to be +specified. + +If any of the other --info-* options are given, the utility will print +information about given data. In this case the input file is read but no +conversion is done and the output file doesn't need to be specified. In case +one data references another and both --info-* options are specified, the output +will also list reference count (for example, --info-scenes together with +--info-meshes will print how many objects reference given mesh). The -i, -c and -m arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the = @@ -421,11 +455,17 @@ well, the IDs reference attributes of the first mesh.)") } /* Generic checks */ - if(args.value("output")) { + if(args.value("input")) { /* Not an error in this case, it should be possible to just append --info* to existing command line without having to remove anything. But print a warning at least, it could also be a mistyped option. */ - if(isInfoRequested(args)) + if(isPluginInfoRequested(args)) + Warning{} << "Ignoring input file for --info:" << args.value("input"); + } + if(args.value("output")) { + /* Same as above, it should be possible to just append --info* to + existing command line */ + if(isPluginInfoRequested(args) || isDataInfoRequested(args)) Warning{} << "Ignoring output file for --info:" << args.value("output"); } if(args.isSet("concatenate-meshes") && args.value("mesh")) { @@ -471,6 +511,40 @@ well, the IDs reference attributes of the first mesh.)") if(args.isSet("verbose")) importer->addFlags(Trade::ImporterFlag::Verbose); Implementation::setOptions(*importer, "AnySceneImporter", args.value("importer-options")); + /* Print plugin info, if requested */ + if(args.isSet("info-importer")) { + Trade::Implementation::printImporterInfo(useColor, *importer); + return 0; + } + if(args.isSet("info-converter")) { + Containers::Pointer converter = converterManager.loadAndInstantiate(args.arrayValueCount("converter") ? args.arrayValue("converter", 0) : "AnySceneConverter"); + if(!converter) { + Debug{} << "Available converter plugins:" << ", "_s.join(converterManager.aliasList()); + return 1; + } + + /* Set options, if passed */ + if(args.isSet("verbose")) converter->addFlags(Trade::SceneConverterFlag::Verbose); + if(args.arrayValueCount("converter-options")) + Implementation::setOptions(*converter, "AnySceneConverter", args.arrayValue("converter-options", 0)); + SceneTools::Implementation::printSceneConverterInfo(useColor, *converter); + return 0; + } + if(args.isSet("info-image-converter")) { + Containers::Pointer converter = imageConverterManager.loadAndInstantiate(args.arrayValueCount("image-converter") ? args.arrayValue("image-converter", 0) : "AnyImageConverter"); + if(!converter) { + Debug{} << "Available image converter plugins:" << ", "_s.join(imageConverterManager.aliasList()); + return 1; + } + + /* Set options, if passed */ + if(args.isSet("verbose")) converter->addFlags(Trade::ImageConverterFlag::Verbose); + if(args.arrayValueCount("image-converter-options")) + Implementation::setOptions(*converter, "AnyImageConverter", args.arrayValue("image-converter-options", 0)); + Trade::Implementation::printImageConverterInfo(useColor, *converter); + return 0; + } + /* Wow, C++, you suck. This implicitly initializes to random shit?! Also, because of addSupportedImporterContents() it's not really possible @@ -499,7 +573,7 @@ well, the IDs reference attributes of the first mesh.)") } /* Print file info, if requested */ - if(isInfoRequested(args)) { + if(isDataInfoRequested(args)) { const bool error = SceneTools::Implementation::printInfo(useColor, useColor24, args, *importer, importConversionTime); if(args.isSet("profile")) { diff --git a/src/Magnum/Trade/Implementation/converterUtilities.h b/src/Magnum/Trade/Implementation/converterUtilities.h index d6dbcf57d..587030592 100644 --- a/src/Magnum/Trade/Implementation/converterUtilities.h +++ b/src/Magnum/Trade/Implementation/converterUtilities.h @@ -26,13 +26,21 @@ */ #include +#include /** @todo remove when Debug is stream-free */ #include #include +#include +#include #include +#include #include +#include /** @todo drop once String::replaceAll() has a StringView overload */ +#include +#include #include "Magnum/PixelFormat.h" #include "Magnum/Trade/AbstractImporter.h" +#include "Magnum/Trade/AbstractImageConverter.h" #include "Magnum/Trade/ImageData.h" namespace Magnum { namespace Trade { namespace Implementation { @@ -41,6 +49,101 @@ namespace Magnum { namespace Trade { namespace Implementation { particular magnum-imageconverter, magnum-sceneconverter and their tests */ namespace { +/** @todo move these to Magnum/Implementation/converterUtilities.h once also + shaderconverter / fontconverter / etc implements --info-converter etc */ +template void printPluginInfo(const Debug::Flags useColor, const T& plugin) { + const PluginManager::PluginMetadata* metadata = plugin.metadata(); + CORRADE_INTERNAL_ASSERT(metadata); + + Debug d{useColor}; + d << Debug::boldColor(Debug::Color::Default) << "Plugin name:" << Debug::boldColor(Debug::Color::Yellow) << metadata->name() << Debug::resetColor; + const std::vector aliases = metadata->provides(); + if(!aliases.empty()) { + d << Debug::newline << Debug::boldColor(Debug::Color::Default) << "Aliases:" << Debug::resetColor; + for(const std::string& alias: aliases) { + d << Debug::newline << " "; + if(alias == plugin.plugin()) + d << Debug::color(Debug::Color::Yellow); + d << alias; + if(alias == plugin.plugin()) + d << Debug::resetColor; + } + } + + /* Ugly, eh? */ + std::ostringstream out; + Debug{&out, Debug::Flag::NoNewlineAtTheEnd} << Debug::packed << plugin.features(); + d << Debug::newline << Debug::boldColor(Debug::Color::Default) << "Features:" << Debug::color(Debug::Color::Cyan) << Debug::newline << " " << Utility::String::replaceAll(out.str(), "|", "\n ") << Debug::resetColor; +} + +void printPluginConfigurationInfo(Debug& d, const Utility::ConfigurationGroup& configuration, const Containers::StringView prefix) { + using namespace Containers::Literals; + + for(Containers::Pair i: configuration.valuesComments()) { + if(i.first()) { + d << Debug::newline << " " << Debug::boldColor(Debug::Color::Blue) << i.first() << Debug::nospace << Debug::color(Debug::Color::Blue) << "=" << Debug::nospace << Debug::color(Debug::Color::Red); + /* Print the value wrapped in quotes if it contains spaces, indent + also all newlines */ + if(i.second().contains('\n')) + /** @todo replaceAll() without std::string */ + d << Utility::format("\"\"\"\n {}\n \"\"\"", Utility::String::replaceAll(i.second(), "\n", "\n ")); + /** @todo less wasteful API for checking leading/trailing zeros? */ + else if(i.second().trimmed() != i.second()) + d << Utility::format("\"{}\"", i.second()); + else + d << i.second(); + d << Debug::resetColor; + } else { + /* Configuration contents are delimited by these markers in order + to include them in Doxygen-generated docs. If we reach them, + it's the end of the (public) configuration values. Don't print + them, don't print even the last newline, and exit. */ + if(i.second() == "# [configuration_]"_s) { + return; + } + + /* Print leading space only if there's actually something */ + d << Debug::newline; + if(i.second()) d << " " << Debug::boldColor(Debug::Color::Black) << i.second() << Debug::resetColor; + } + } + + for(Containers::Pair> i: configuration.groups()) { + const Containers::String name = prefix ? "/"_s.join({prefix, i.first()}) : Containers::String{i.first()}; + + d << Debug::newline << " " << Debug::color(Debug::Color::Blue) << "[" << Debug::nospace << Debug::boldColor(Debug::Color::Blue) << name << Debug::color(Debug::Color::Blue) << Debug::nospace << "]" << Debug::resetColor; + + printPluginConfigurationInfo(d, i.second(), name); + } +} + +void printPluginConfigurationInfo(const Debug::Flags useColor, const PluginManager::AbstractPlugin& plugin) { + const Utility::ConfigurationGroup& configuration = plugin.configuration(); + if(configuration.isEmpty()) return; + + Debug d{useColor}; + d << Debug::boldColor(Debug::Color::Default) << "Configuration:" << Debug::resetColor; + printPluginConfigurationInfo(d, configuration, {}); +} + +void printImporterInfo(const Debug::Flags useColor, const Trade::AbstractImporter& importer) { + printPluginInfo(useColor, importer); + printPluginConfigurationInfo(useColor, importer); +} + +void printImageConverterInfo(const Debug::Flags useColor, const Trade::AbstractImageConverter& converter) { + printPluginInfo(useColor, converter); + + Debug d{useColor|Debug::Flag::NoNewlineAtTheEnd}; + + if(const Containers::String extension = converter.extension()) + d << Debug::boldColor(Debug::Color::Default) << "File extension:" << Debug::resetColor << extension << Debug::newline; + if(const Containers::String mimeType = converter.mimeType()) + d << Debug::boldColor(Debug::Color::Default) << "MIME type:" << Debug::resetColor << mimeType << Debug::newline; + + printPluginConfigurationInfo(useColor, converter); +} + struct Duration { explicit Duration(std::chrono::high_resolution_clock::duration& output): _output(output), _t{std::chrono::high_resolution_clock::now()} {} diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index 9ee972963..8351cf12e 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -40,6 +40,17 @@ endif() if(MAGNUM_WITH_IMAGECONVERTER AND CORRADE_TARGET_UNIX) set(IMAGECONVERTER_EXECUTABLE_FILENAME $) endif() +if(NOT MAGNUM_BUILD_PLUGINS_STATIC) + if(MAGNUM_WITH_ANYIMAGEIMPORTER AND NOT MAGNUM_ANYIMAGEIMPORTER_BUILD_STATIC) + set(ANYIMAGEIMPORTER_PLUGIN_FILENAME $) + endif() + if(MAGNUM_WITH_ANYIMAGECONVERTER AND NOT MAGNUM_ANYIMAGECONVERTER_BUILD_STATIC) + set(ANYIMAGECONVERTER_PLUGIN_FILENAME $) + endif() + if(MAGNUM_WITH_TGAIMAGECONVERTER AND NOT MAGNUM_TGAIMAGECONVERTER_BUILD_STATIC) + set(TGAIMAGECONVERTER_PLUGIN_FILENAME $) + endif() +endif() # First replace ${} variables, then $<> generator expressions configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake @@ -69,6 +80,30 @@ corrade_add_test(TradeImageConverterImplementa___Test ImageConverterImplementati FILES ImageConverterImplementationTestFiles/info.txt) target_include_directories(TradeImageConverterImplementa___Test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +if(MAGNUM_WITH_ANYIMAGEIMPORTER) + if(MAGNUM_BUILD_PLUGINS_STATIC OR MAGNUM_ANYIMAGEIMPORTER_BUILD_STATIC) + target_link_libraries(TradeImageConverterImplementa___Test PRIVATE AnyImageImporter) + else() + # So the plugins get properly built when building the test + add_dependencies(TradeImageConverterImplementa___Test AnyImageImporter) + endif() +endif() +if(MAGNUM_WITH_ANYIMAGECONVERTER) + if(MAGNUM_BUILD_PLUGINS_STATIC OR MAGNUM_ANYIMAGECONVERTER_BUILD_STATIC) + target_link_libraries(TradeImageConverterImplementa___Test PRIVATE AnyImageConverter) + else() + # So the plugins get properly built when building the test + add_dependencies(TradeImageConverterImplementa___Test AnyImageConverter) + endif() +endif() +if(MAGNUM_WITH_TGAIMAGECONVERTER) + if(MAGNUM_BUILD_PLUGINS_STATIC OR MAGNUM_TGAIMAGECONVERTER_BUILD_STATIC) + target_link_libraries(TradeImageConverterImplementa___Test PRIVATE TgaImageConverter) + else() + # So the plugins get properly built when building the test + add_dependencies(TradeImageConverterImplementa___Test TgaImageConverter) + endif() +endif() # Executable testing is implemented on Unix platforms only at the moment if(CORRADE_TARGET_UNIX AND NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) @@ -81,6 +116,9 @@ if(CORRADE_TARGET_UNIX AND NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) FILES ImageConverterTestFiles/info-data.txt ImageConverterTestFiles/info-data-ignored-output.txt + ImageConverterTestFiles/info-converter.txt + ImageConverterTestFiles/info-importer.txt + ImageConverterTestFiles/info-importer-ignored-input-output.txt ImageConverterTestFiles/file.tga) target_include_directories(TradeImageConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) if(MAGNUM_WITH_IMAGECONVERTER) diff --git a/src/Magnum/Trade/Test/ImageConverterImplementationTest.cpp b/src/Magnum/Trade/Test/ImageConverterImplementationTest.cpp index be66e56ea..f83db6201 100644 --- a/src/Magnum/Trade/Test/ImageConverterImplementationTest.cpp +++ b/src/Magnum/Trade/Test/ImageConverterImplementationTest.cpp @@ -27,6 +27,7 @@ #include /** @todo remove once Debug is stream-free */ #include #include +#include #include /** @todo remove once Debug is stream-free */ #include @@ -39,13 +40,325 @@ namespace Magnum { namespace Trade { namespace Test { namespace { struct ImageConverterImplementationTest: TestSuite::Tester { explicit ImageConverterImplementationTest(); + void pluginInfo(); + void pluginInfoAliases(); + void pluginConfigurationInfoEmpty(); + void pluginConfigurationInfo(); + void pluginConfigurationInfoDoxygenDelimiter(); + void importerInfo(); + void converterInfo(); + void converterInfoExtensionMimeType(); + void info(); void infoError(); + + /* Explicitly forbid system-wide plugin dependencies */ + PluginManager::Manager _importerManager{"nonexistent"}; + PluginManager::Manager _converterManager{"nonexistent"}; }; ImageConverterImplementationTest::ImageConverterImplementationTest() { - addTests({&ImageConverterImplementationTest::info, + addTests({&ImageConverterImplementationTest::pluginInfo, + &ImageConverterImplementationTest::pluginInfoAliases, + &ImageConverterImplementationTest::pluginConfigurationInfoEmpty, + &ImageConverterImplementationTest::pluginConfigurationInfo, + &ImageConverterImplementationTest::pluginConfigurationInfoDoxygenDelimiter, + &ImageConverterImplementationTest::importerInfo, + &ImageConverterImplementationTest::converterInfo, + &ImageConverterImplementationTest::converterInfoExtensionMimeType, + + &ImageConverterImplementationTest::info, &ImageConverterImplementationTest::infoError}); + + /* Load the plugin directly from the build tree. Otherwise it's static and + already loaded. */ + #ifdef ANYIMAGEIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_importerManager.load(ANYIMAGEIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + #ifdef ANYIMAGECONVERTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_converterManager.load(ANYIMAGECONVERTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + #ifdef TGAIMAGECONVERTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_converterManager.load(TGAIMAGECONVERTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif +} + +void ImageConverterImplementationTest::pluginInfo() { + /* Check if the required plugin can be loaded. Catches also ABI and + interface mismatch errors. */ + if(!(_converterManager.load("AnyImageConverter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("AnyImageConverter plugin can't be loaded."); + + Containers::Pointer converter = _converterManager.instantiate("AnyImageConverter"); + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printPluginInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, *converter); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printPluginInfo(Debug::Flag::DisableColors, *converter); + CORRADE_COMPARE(out.str(), + "Plugin name: AnyImageConverter\n" + "Features:\n" + " Convert1DToFile\n" + " Convert2DToFile\n" + " Convert3DToFile\n" + " ConvertCompressed1DToFile\n" + " ConvertCompressed2DToFile\n" + " ConvertCompressed3DToFile\n" + " Levels\n"); +} + +void ImageConverterImplementationTest::pluginInfoAliases() { + PluginManager::Manager importerManager{MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR}; + + /* Check if the required plugin can be loaded. Catches also ABI and + interface mismatch errors. */ + if(!(importerManager.load("StbImageImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("StbImageImporter plugin can't be loaded."); + + /* Loading under an alias to verify that it's highlighted. Make + StbImageImporter *the* plugin to load PPMs, so it's not replaced by e.g. + DevIlImageImporter. */ + importerManager.setPreferredPlugins("PpmImporter", {"StbImageImporter"}); + Containers::Pointer importer = importerManager.instantiate("PpmImporter"); + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printPluginInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, *importer); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printPluginInfo(Debug::Flag::DisableColors, *importer); + CORRADE_COMPARE(out.str(), + "Plugin name: StbImageImporter\n" + "Aliases:\n" + " BmpImporter\n" + " GifImporter\n" + " HdrImporter\n" + " JpegImporter\n" + " PgmImporter\n" + " PicImporter\n" + " PngImporter\n" + " PpmImporter\n" + " PsdImporter\n" + " TgaImporter\n" + "Features:\n" + " OpenData\n"); +} + +void ImageConverterImplementationTest::pluginConfigurationInfoEmpty() { + struct: Trade::AbstractImporter { + Trade::ImporterFeatures doFeatures() const override { + return Trade::ImporterFeature::FileCallback| + Trade::ImporterFeature::OpenState; + } + bool doIsOpened() const override { return false; } + void doClose() override {} + } importer; + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printPluginConfigurationInfo(Debug::Flag::DisableColors, importer); + CORRADE_COMPARE(out.str(), ""); +} + +void ImageConverterImplementationTest::pluginConfigurationInfo() { + struct: Trade::AbstractImporter { + Trade::ImporterFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return false; } + void doClose() override {} + } importer; + + std::stringstream in; + in << R"([configuration] +# A comment +; Another +value=yes +another=42 +# Empty lines should not have trailing whitespace + +[configuration/group] +spaces=" YES " +newlines=""" +A + L + S + O +""" + +[configuration/group/subgroup] +subvalue=35 + +# Another instance of the same group +[configuration/group] +true=false +)"; + Utility::Configuration conf{in}; + + importer.configuration() = Utility::ConfigurationGroup{*conf.group("configuration")}; + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printPluginConfigurationInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, importer); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printPluginConfigurationInfo(Debug::Flag::DisableColors, importer); + CORRADE_COMPARE(out.str(), + "Configuration:\n" + " # A comment\n" + " ; Another\n" + " value=yes\n" + " another=42\n" + " # Empty lines should not have trailing whitespace\n" + "\n" + " [group]\n" + " spaces=\" YES \"\n" + " newlines=\"\"\"\n" + " A\n" + " L\n" + " S\n" + " O\n" + " \"\"\"\n" + "\n" + " [group/subgroup]\n" + " subvalue=35\n" + "\n" + " # Another instance of the same group\n" + " [group]\n" + " true=false\n"); +} + +void ImageConverterImplementationTest::pluginConfigurationInfoDoxygenDelimiter() { + struct: Trade::AbstractImporter { + Trade::ImporterFeatures doFeatures() const override { return {}; } + bool doIsOpened() const override { return false; } + void doClose() override {} + } importer; + + std::stringstream in; + in << R"(# [configuration_] +[configuration] +# A comment +value=yes +# [configuration_] +privateValue=SECRET +)"; + Utility::Configuration conf{in}; + + importer.configuration() = Utility::ConfigurationGroup{*conf.group("configuration")}; + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printPluginConfigurationInfo(Debug::Flag::DisableColors, importer); + CORRADE_COMPARE(out.str(), + "Configuration:\n" + " # A comment\n" + " value=yes\n"); +} + +void ImageConverterImplementationTest::importerInfo() { + /* Check if the required plugin can be loaded. Catches also ABI and + interface mismatch errors. */ + if(!(_importerManager.load("AnyImageImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("AnyImageImporter plugin can't be loaded."); + + Containers::Pointer importer = _importerManager.instantiate("AnyImageImporter"); + /** @todo pick a plugin that has some actual configuration */ + importer->configuration().setValue("something", "is there"); + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printImporterInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, *importer); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printImporterInfo(Debug::Flag::DisableColors, *importer); + CORRADE_COMPARE(out.str(), + "Plugin name: AnyImageImporter\n" + "Features:\n" + " OpenData\n" + " FileCallback\n" + "Configuration:\n" + " something=is there\n"); +} + +void ImageConverterImplementationTest::converterInfo() { + /* Check if the required plugin can be loaded. Catches also ABI and + interface mismatch errors. */ + if(!(_converterManager.load("AnyImageConverter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("AnyImageConverter plugin can't be loaded."); + + Containers::Pointer converter = _converterManager.instantiate("AnyImageConverter"); + /** @todo pick a plugin that has some actual configuration */ + converter->configuration().setValue("something", "is there"); + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printImageConverterInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, *converter); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printImageConverterInfo(Debug::Flag::DisableColors, *converter); + CORRADE_COMPARE(out.str(), + "Plugin name: AnyImageConverter\n" + "Features:\n" + " Convert1DToFile\n" + " Convert2DToFile\n" + " Convert3DToFile\n" + " ConvertCompressed1DToFile\n" + " ConvertCompressed2DToFile\n" + " ConvertCompressed3DToFile\n" + " Levels\n" + "Configuration:\n" + " something=is there\n"); +} + +void ImageConverterImplementationTest::converterInfoExtensionMimeType() { + /* Check if the required plugin can be loaded. Catches also ABI and + interface mismatch errors. */ + if(!(_converterManager.load("TgaImageConverter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("TgaImageConverter plugin can't be loaded."); + + Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); + /** @todo pick a plugin that has some actual configuration */ + converter->configuration().setValue("something", "is there"); + + /* Print to visually verify coloring */ + { + Debug{} << "======================== visual color verification start ======================="; + Implementation::printImageConverterInfo(Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors, *converter); + Debug{} << "======================== visual color verification end ========================="; + } + + std::ostringstream out; + Debug redirectOutput{&out}; + Implementation::printImageConverterInfo(Debug::Flag::DisableColors, *converter); + CORRADE_COMPARE(out.str(), + "Plugin name: TgaImageConverter\n" + "Features:\n" + " Convert2DToData\n" + "File extension: tga\n" + "MIME type: image/x-tga\n" + "Configuration:\n" + " something=is there\n"); } void ImageConverterImplementationTest::info() { diff --git a/src/Magnum/Trade/Test/ImageConverterTest.cpp b/src/Magnum/Trade/Test/ImageConverterTest.cpp index b239d90a7..56aa7be8b 100644 --- a/src/Magnum/Trade/Test/ImageConverterTest.cpp +++ b/src/Magnum/Trade/Test/ImageConverterTest.cpp @@ -34,6 +34,7 @@ #include #include "Magnum/Trade/AbstractImporter.h" +#include "Magnum/Trade/AbstractImageConverter.h" #include "configure.h" @@ -51,21 +52,38 @@ const struct { const char* name; Containers::Array args; const char* requiresImporter; + const char* requiresConverter; const char* expected; } InfoData[]{ + {"importer", Containers::array({ + "--info-importer", "-i", "someOption=yes"}), + "AnyImageImporter", nullptr, + "info-importer.txt"}, + {"converter", Containers::array({ + "-C", "AnyImageConverter", "--info-converter", "-c", "someOption=yes"}), + nullptr, "AnyImageConverter", + "info-converter.txt"}, + {"converter, implicit", Containers::array({ + "--info-converter", "-c", "someOption=yes"}), + nullptr, "AnyImageConverter", + "info-converter.txt"}, + {"importer, ignored input and output", Containers::array({ + "--info-importer", "a.png", "b.png", "out.jpg"}), + "AnySceneImporter", nullptr, + "info-importer-ignored-input-output.txt"}, {"data", Containers::array({ "-I", "TgaImporter", "--info", Utility::Path::join(TRADE_TEST_DIR, "ImageConverterTestFiles/file.tga")}), - "TgaImporter", + "TgaImporter", nullptr, "info-data.txt"}, {"data, map", Containers::array({ "--map", "-I", "TgaImporter", "--info", Utility::Path::join(TRADE_TEST_DIR, "ImageConverterTestFiles/file.tga")}), - "TgaImporter", + "TgaImporter", nullptr, /** @todo change to something else once we have a plugin that can zero-copy pass the imported data */ "info-data.txt"}, {"data, ignored output file", Containers::array({ "-I", "TgaImporter", "--info", Utility::Path::join(TRADE_TEST_DIR, "ImageConverterTestFiles/file.tga"), "whatever.png"}), - "TgaImporter", + "TgaImporter", nullptr, "info-data-ignored-output.txt"} }; @@ -122,8 +140,11 @@ void ImageConverterTest::info() { /* Check if required plugins can be loaded. Catches also ABI and interface mismatch errors. */ PluginManager::Manager importerManager{MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR}; + PluginManager::Manager converterManager{MAGNUM_PLUGINS_IMAGECONVERTER_INSTALL_DIR}; if(data.requiresImporter && !(importerManager.load(data.requiresImporter) & PluginManager::LoadState::Loaded)) CORRADE_SKIP(data.requiresImporter << "plugin can't be loaded."); + if(data.requiresConverter && !(converterManager.load(data.requiresConverter) & PluginManager::LoadState::Loaded)) + CORRADE_SKIP(data.requiresConverter << "plugin can't be loaded."); CORRADE_VERIFY(true); /* capture correct function name */ diff --git a/src/Magnum/Trade/Test/ImageConverterTestFiles/info-converter.txt b/src/Magnum/Trade/Test/ImageConverterTestFiles/info-converter.txt new file mode 100644 index 000000000..3ab9b48e2 --- /dev/null +++ b/src/Magnum/Trade/Test/ImageConverterTestFiles/info-converter.txt @@ -0,0 +1,11 @@ +Plugin name: AnyImageConverter +Features: + Convert1DToFile + Convert2DToFile + Convert3DToFile + ConvertCompressed1DToFile + ConvertCompressed2DToFile + ConvertCompressed3DToFile + Levels +Configuration: + someOption=yes diff --git a/src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer-ignored-input-output.txt b/src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer-ignored-input-output.txt new file mode 100644 index 000000000..fd93eea9b --- /dev/null +++ b/src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer-ignored-input-output.txt @@ -0,0 +1,6 @@ +Ignoring input files for --info: a.png b.png +Ignoring output file for --info: out.jpg +Plugin name: AnyImageImporter +Features: + OpenData + FileCallback diff --git a/src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer.txt b/src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer.txt new file mode 100644 index 000000000..ce49ddde9 --- /dev/null +++ b/src/Magnum/Trade/Test/ImageConverterTestFiles/info-importer.txt @@ -0,0 +1,6 @@ +Plugin name: AnyImageImporter +Features: + OpenData + FileCallback +Configuration: + someOption=yes diff --git a/src/Magnum/Trade/Test/configure.h.cmake b/src/Magnum/Trade/Test/configure.h.cmake index f328ef8db..773af0c07 100644 --- a/src/Magnum/Trade/Test/configure.h.cmake +++ b/src/Magnum/Trade/Test/configure.h.cmake @@ -26,21 +26,28 @@ #define TRADE_TEST_DIR "${TRADE_TEST_DIR}" #define TRADE_TEST_OUTPUT_DIR "${TRADE_TEST_OUTPUT_DIR}" #cmakedefine IMAGECONVERTER_EXECUTABLE_FILENAME "${IMAGECONVERTER_EXECUTABLE_FILENAME}" +#cmakedefine ANYIMAGEIMPORTER_PLUGIN_FILENAME "${ANYIMAGEIMPORTER_PLUGIN_FILENAME}" +#cmakedefine ANYIMAGECONVERTER_PLUGIN_FILENAME "${ANYIMAGECONVERTER_PLUGIN_FILENAME}" +#cmakedefine TGAIMAGECONVERTER_PLUGIN_FILENAME "${TGAIMAGECONVERTER_PLUGIN_FILENAME}" #ifdef CORRADE_TARGET_WINDOWS #ifdef CORRADE_IS_DEBUG_BUILD #define MAGNUM_PLUGINS_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}" #define MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMPORTER_DEBUG_BINARY_INSTALL_DIR}" +#define MAGNUM_PLUGINS_IMAGECONVERTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_BINARY_INSTALL_DIR}" #else #define MAGNUM_PLUGINS_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}" #define MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMPORTER_RELEASE_BINARY_INSTALL_DIR}" +#define MAGNUM_PLUGINS_IMAGECONVERTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMAGECONVERTER_RELEASE_BINARY_INSTALL_DIR}" #endif #else #ifdef CORRADE_IS_DEBUG_BUILD #define MAGNUM_PLUGINS_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}" #define MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMPORTER_DEBUG_LIBRARY_INSTALL_DIR}" +#define MAGNUM_PLUGINS_IMAGECONVERTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_LIBRARY_INSTALL_DIR}" #else #define MAGNUM_PLUGINS_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}" #define MAGNUM_PLUGINS_IMPORTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMPORTER_RELEASE_LIBRARY_INSTALL_DIR}" +#define MAGNUM_PLUGINS_IMAGECONVERTER_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${MAGNUM_PLUGINS_IMAGECONVERTER_RELEASE_LIBRARY_INSTALL_DIR}" #endif #endif diff --git a/src/Magnum/Trade/imageconverter.cpp b/src/Magnum/Trade/imageconverter.cpp index db9d2f725..026f15bc8 100644 --- a/src/Magnum/Trade/imageconverter.cpp +++ b/src/Magnum/Trade/imageconverter.cpp @@ -192,7 +192,8 @@ magnum-imageconverter [-h|--help] [-I|--importer PLUGIN] [-i|--importer-options key=val,key2=val2,…] [-c|--converter-options key=val,key2=val2,…]... [-D|--dimensions N] [--image N] [--level N] [--layer N] [--layers] [--levels] [--in-place] - [--info] [--color on|off|auto] [-v|--verbose] [--profile] [--] input output + [--info-importer] [--info-converter] [--info] [--color on|off|auto] + [-v|--verbose] [--profile] [--] input output @endcode Arguments: @@ -221,6 +222,8 @@ Arguments: more - `--levels` --- combine multiple image levels into a single file - `--in-place` --- overwrite the input image with the output +- `--info-importer` --- print info about the importer plugin and exit +- `--info-converter` --- print info about the image converter plugin and exit - `--info` --- print info about the input file and exit - `--color` --- colored output for `--info` (default: `auto`) - `-v`, `--verbose` --- verbose output from importer and converter plugins @@ -231,9 +234,16 @@ tightly-packed square of pixels in given @ref PixelFormat. Specifying `-C` / `--converter raw` will save raw imported data instead of using a converter plugin. -If `--info` is given, the utility will print information about all images -present in the file, independently of the `-D` / `--dimensions` option. In this -case no conversion is done and output file doesn't need to be specified. +If the `--info-importer` or `--info-converter` option is given, the utility +will print information about given plugin specified via the `-I` or `-C` +option, including its configuration options potentially overriden with +`-i` or `-c`. In this case no file is read and no conversion is done and +neither the input nor the output file needs to be specified. + +If `--info` is given, the utility will print information about given data, +independently of the `-D` / `--dimensions` option. In this case the input file +is read but no no conversion is done and output file doesn't need to be +specified. The `-i` / `--importer-options` and `-c` / `--converter-options` arguments accept a comma-separated list of key/value pairs to set in the importer / @@ -258,6 +268,11 @@ using namespace Containers::Literals; namespace { +bool isPluginInfoRequested(const Utility::Arguments& args) { + return args.isSet("info-importer") || + args.isSet("info-converter"); +} + template bool checkCommonFormatFlags(const Utility::Arguments& args, const Containers::Array>& images) { CORRADE_INTERNAL_ASSERT(!images.isEmpty()); const bool compressed = images.front().isCompressed(); @@ -362,15 +377,21 @@ int main(int argc, char** argv) { .addBooleanOption("layers").setHelp("layers", "combine multiple layers into an image with one dimension more") .addBooleanOption("levels").setHelp("layers", "combine multiple image levels into a single file") .addBooleanOption("in-place").setHelp("in-place", "overwrite the input image with the output") + .addBooleanOption("info-importer").setHelp("info-importer", "print info about the importer plugin and exit") + .addBooleanOption("info-converter").setHelp("info-converter", "print info about the image converter plugin and exit") .addBooleanOption("info").setHelp("info", "print info about the input file and exit") .addOption("color", "auto").setHelp("color", "colored output for --info", "on|off|auto") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins") .addBooleanOption("profile").setHelp("profile", "measure import and conversion time") .setParseErrorCallback([](const Utility::Arguments& args, Utility::Arguments::ParseError error, const std::string& key) { - /* If --in-place or --info is passed, we don't need the output - argument */ + /* If --info for plugins is passed, we don't need the input */ if(error == Utility::Arguments::ParseError::MissingArgument && - key == "output" && (args.isSet("in-place") || args.isSet("info"))) + key == "input" && isPluginInfoRequested(args)) + return true; + /* If --in-place or --info for plugins or data is passed, we don't + need the output argument */ + if(error == Utility::Arguments::ParseError::MissingArgument && + key == "output" && (args.isSet("in-place") || isPluginInfoRequested(args) || args.isSet("info"))) return true; /* Handle all other errors as usual */ @@ -382,9 +403,14 @@ Specifying --importer raw: will treat the input as a raw tightly-packed square of pixels in given pixel format. Specifying -C / --converter raw will save raw imported data instead of using a converter plugin. -If --info is given, the utility will print information about all images present -in the file, independently of the -D / --dimensions option. In this case no -conversion is done and output file doesn't need to be specified. +If the --info-importer or --info-converter option is given, the utility will +print information about given plugin specified via the -I or -C option, +including its configuration options potentially overriden with -i or -c. In +this case no file is read and no conversion is done and neither the input nor +the output file needs to be specified. + +If --info is given, the utility will print information about given data, independently of the -D / --dimensions option. In this case the input file is +read but no conversion is done and output file doesn't need to be specified. The -i / --importer-options and -c / --converter-options arguments accept a comma-separated list of key/value pairs to set in the importer / converter @@ -410,16 +436,26 @@ no -C / --converter is specified, AnyImageConverter is used.)") useColor = Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors; /* Generic checks */ + if(const std::size_t inputCount = args.arrayValueCount("input")) { + /* Not an error in this case, it should be possible to just append + --info* to existing command line without having to remove anything. + But print a warning at least, it could also be a mistyped option. */ + if(isPluginInfoRequested(args)) { + Warning w; + w << "Ignoring input files for --info:"; + for(std::size_t i = 0; i != inputCount; ++i) + w << args.arrayValue("input", i); + } + } if(args.value("output")) { if(args.isSet("in-place")) { Error{} << "Output file shouldn't be set for --in-place:" << args.value("output"); return 1; } - /* Not an error in this case, it should be possible to just append - --info to existing command line without having to remove anything. - But print a warning at least, it could also be a mistyped option. */ - if(args.isSet("info")) + /* Same as above, it should be possible to just append --info* to + existing command line */ + if(isPluginInfoRequested(args) || args.isSet("info")) Warning{} << "Ignoring output file for --info:" << args.value("output"); } @@ -450,7 +486,7 @@ no -C / --converter is specified, AnyImageConverter is used.)") Error{} << "The --levels option can't be combined with raw data output"; return 1; } - if(!args.isSet("layers") && !args.isSet("levels") && args.arrayValueCount("input") > 1) { + if(!args.isSet("layers") && !args.isSet("levels") && args.arrayValueCount("input") > 1 && !isPluginInfoRequested(args)) { Error{} << "Multiple input files require the --layers / --levels option to be set"; return 1; } @@ -463,6 +499,35 @@ no -C / --converter is specified, AnyImageConverter is used.)") args.value("plugin-dir").empty() ? Containers::String{} : Utility::Path::join(args.value("plugin-dir"), Utility::Path::split(Trade::AbstractImageConverter::pluginSearchPaths().back()).second())}; + /* Print plugin info, if requested */ + if(args.isSet("info-importer")) { + Containers::Pointer importer = importerManager.loadAndInstantiate(args.value("importer")); + if(!importer) { + Debug{} << "Available importer plugins:" << ", "_s.join(importerManager.aliasList()); + return 1; + } + + /* Set options, if passed */ + if(args.isSet("verbose")) importer->addFlags(Trade::ImporterFlag::Verbose); + Implementation::setOptions(*importer, "AnyImageImporter", args.value("importer-options")); + Trade::Implementation::printImporterInfo(useColor, *importer); + return 0; + } + if(args.isSet("info-converter")) { + Containers::Pointer converter = converterManager.loadAndInstantiate(args.arrayValueCount("converter") ? args.arrayValue("converter", 0) : "AnyImageConverter"); + if(!converter) { + Debug{} << "Available converter plugins:" << ", "_s.join(converterManager.aliasList()); + return 1; + } + + /* Set options, if passed */ + if(args.isSet("verbose")) converter->addFlags(Trade::ImageConverterFlag::Verbose); + if(args.arrayValueCount("converter-options")) + Implementation::setOptions(*converter, "AnyImageConverter", args.arrayValue("converter-options", 0)); + Trade::Implementation::printImageConverterInfo(useColor, *converter); + return 0; + } + const Int dimensions = args.value("dimensions"); /** @todo make them array options as well? */ const UnsignedInt image = args.value("image");