diff --git a/doc/changelog.dox b/doc/changelog.dox index 85caf99e3..70d25d1a2 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -208,7 +208,8 @@ See also: are encouraged over @relativeref{Trade::AbstractImporter,setFlags()} as it avoid accidentally clearing default flags potentially added in the future. - Ability to convert also 1D and 3D images with the - @ref magnum-imageconverter "magnum-imageconverter" utility. + @ref magnum-imageconverter "magnum-imageconverter" utility, as well as + combining layers into images of one dimension more @subsubsection changelog-latest-new-vk Vk library diff --git a/src/Magnum/Trade/imageconverter.cpp b/src/Magnum/Trade/imageconverter.cpp index 75629d5a5..b7bc2d31c 100644 --- a/src/Magnum/Trade/imageconverter.cpp +++ b/src/Magnum/Trade/imageconverter.cpp @@ -26,8 +26,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -69,7 +71,7 @@ magnum-imageconverter [-h|--help] [-I|--importer PLUGIN] [-C|--converter PLUGIN] [--plugin-dir DIR] [-i|--importer-options key=val,key2=val2,…] [-c|--converter-options key=val,key2=val2,…] [-D|--dimensions N] - [--image N] [--level N] [--in-place] [--info] [-v|--verbose] + [--image N] [--level N] [--layers] [--in-place] [--info] [-v|--verbose] [--] input output @endcode @@ -92,6 +94,8 @@ Arguments: (default: `2`) - `--image N` --- image to import (default: `0`) - `--level N` --- image level to import (default: `0`) +- `--layers` --- combine multiple layers into an image with one dimension + more - `--in-place` --- overwrite the input image with the output - `--info` --- print info about the input file and exit - `-v`, `--verbose` --- verbose output from importer and converter plugins @@ -146,9 +150,58 @@ magnum-imageconverter image.dds --converter raw data.dat using namespace Magnum; +namespace { + +template bool checkCommonFormat(const Utility::Arguments& args, const Containers::Array>& images) { + CORRADE_INTERNAL_ASSERT(!images.empty()); + const bool compressed = images.front().isCompressed(); + PixelFormat format{}; + CompressedPixelFormat compressedFormat{}; + if(!compressed) format = images.front().format(); + else compressedFormat = images.front().compressedFormat(); + for(std::size_t i = 1; i != images.size(); ++i) { + if(images[i].isCompressed() != compressed || + (!compressed && images[i].format() != format) || + (compressed && images[i].compressedFormat() != compressedFormat)) + { + Error e; + e << "Images have different formats," << args.arrayValue("input", i) << "has"; + if(images[i].isCompressed()) + e << images[i].compressedFormat(); + else + e << images[i].format(); + e << Debug::nospace << ", expected"; + if(compressed) + e << compressedFormat; + else + e << format; + return false; + } + } + + return true; +} + +template bool checkCommonFormatAndSize(const Utility::Arguments& args, const Containers::Array>& images) { + if(!checkCommonFormat(args, images)) return false; + + CORRADE_INTERNAL_ASSERT(!images.empty()); + Math::Vector size = images.front().size(); + for(std::size_t i = 1; i != images.size(); ++i) { + if(images[i].size() != size) { + Error{} << "Images have different sizes," << args.arrayValue("input", i) << "has a size of" << images[i].size() << Debug::nospace << ", expected" << size; + return false; + } + } + + return true; +} + +} + int main(int argc, char** argv) { Utility::Arguments args; - args.addArgument("input").setHelp("input", "input image") + args.addArrayArgument("input").setHelp("input", "input image(s)") .addArgument("output").setHelp("output", "output image; ignored if --info is present, disallowed for --in-place") .addOption('I', "importer", "AnyImageImporter").setHelp("importer", "image importer plugin", "PLUGIN") .addOption('C', "converter", "AnyImageConverter").setHelp("converter", "image converter plugin", "PLUGIN") @@ -158,6 +211,7 @@ int main(int argc, char** argv) { .addOption('D', "dimensions", "2").setHelp("dimensions", "import and convert image of given dimensions", "N") .addOption("image", "0").setHelp("image", "image to import", "N") .addOption("level", "0").setHelp("level", "image level to import", "N") + .addBooleanOption("layers").setHelp("layers", "combine multiple layers into an image with one dimension more") .addBooleanOption("in-place").setHelp("in-place", "overwrite the input image with the output") .addBooleanOption("info").setHelp("info", "print info about the input file and exit") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins") @@ -201,182 +255,306 @@ key=true; configuration subgroups are delimited with /.)") Warning{} << "Ignoring output file for --info:" << args.value("output"); } + /* Mutually incompatible options */ + if(args.isSet("layers") && args.isSet("in-place")) { + Error{} << "The --layers option can't be combined with --in-place"; + return 1; + } + if(args.isSet("layers") && args.isSet("info")) { + Error{} << "The --layers option can't be combined with --info"; + return 1; + } + if(!args.isSet("layers") && args.arrayValueCount("input") > 1) { + Error{} << "Multiple input files require the --layers option to be set"; + return 1; + } + PluginManager::Manager importerManager{ args.value("plugin-dir").empty() ? std::string{} : Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractImporter::pluginSearchPaths()[0])}; - /* Load raw data, if requested; assume it's a tightly-packed square of - given format */ - /** @todo implement image slicing and then use `--slice "0 0 w h"` to - specify non-rectangular size (and +x +y to specify padding?) */ - const std::string input = args.value("input"); const Int dimensions = args.value("dimensions"); + /** @todo make them array options as well? */ const UnsignedInt image = args.value("image"); const UnsignedInt level = args.value("level"); - Containers::Optional image1D; - Containers::Optional image2D; - Containers::Optional image3D; - if(Utility::String::beginsWith(args.value("importer"), "raw:")) { - if(dimensions != 2) { - Error{} << "Raw data inputs can be only used for 2D images"; - return 1; - } + Containers::Array images1D; + Containers::Array images2D; + Containers::Array images3D; + + for(std::size_t i = 0, max = args.arrayValueCount("input"); i != max; ++i) { + const std::string input = args.arrayValue("input", i); + + /* Load raw data, if requested; assume it's a tightly-packed square of + given format */ + /** @todo implement image slicing and then use `--slice "0 0 w h"` to + specify non-rectangular size (and +x +y to specify padding?) */ + if(Utility::String::beginsWith(args.value("importer"), "raw:")) { + if(dimensions != 2) { + Error{} << "Raw data inputs can be only used for 2D images"; + return 1; + } - /** @todo Any chance to do this without using internal APIs? */ - const PixelFormat format = Utility::ConfigurationValue::fromString(args.value("importer").substr(4), {}); - const UnsignedInt pixelSize = Magnum::pixelSize(format); - if(format == PixelFormat{}) { - Error{} << "Invalid raw pixel format" << args.value("importer"); - return 4; - } + /** @todo Any chance to do this without using internal APIs? */ + const PixelFormat format = Utility::ConfigurationValue::fromString(args.value("importer").substr(4), {}); + const UnsignedInt pixelSize = Magnum::pixelSize(format); + if(format == PixelFormat{}) { + Error{} << "Invalid raw pixel format" << args.value("importer"); + return 4; + } - /** @todo simplify once read() reliably returns an Optional */ - if(!Utility::Directory::exists(input)) { - Error{} << "Cannot open file" << input; - return 3; - } - Containers::Array data = Utility::Directory::read(input); - auto side = Int(std::sqrt(data.size()/pixelSize)); - if(data.size() % pixelSize || side*side*pixelSize != data.size()) { - Error{} << "File of size" << data.size() << "is not a tightly-packed square of" << format; - return 5; - } + /** @todo simplify once read() reliably returns an Optional */ + if(!Utility::Directory::exists(input)) { + Error{} << "Cannot open file" << input; + return 3; + } + Containers::Array data = Utility::Directory::read(input); + auto side = Int(std::sqrt(data.size()/pixelSize)); + if(data.size() % pixelSize || side*side*pixelSize != data.size()) { + Error{} << "File of size" << data.size() << "is not a tightly-packed square of" << format; + return 5; + } - /* Print image info, if requested */ - if(args.isSet("info")) { - Debug{} << "Image 0:\n Mip 0:" << format << Vector2i{side}; - return 0; - } + /* Print image info, if requested */ + if(args.isSet("info")) { + Debug{} << "Image 0:\n Mip 0:" << format << Vector2i{side}; + return 0; + } - image2D = Trade::ImageData2D(format, Vector2i{side}, std::move(data)); + arrayAppend(images2D, InPlaceInit, format, Vector2i{side}, std::move(data)); - /* Otherwise load it using an importer plugin */ - } else { - Containers::Pointer importer = importerManager.loadAndInstantiate(args.value("importer")); - if(!importer) { - Debug{} << "Available importer plugins:" << Utility::String::join(importerManager.aliasList(), ", "); - return 1; - } + /* Otherwise load it using an importer plugin */ + } else { + Containers::Pointer importer = importerManager.loadAndInstantiate(args.value("importer")); + if(!importer) { + Debug{} << "Available importer plugins:" << Utility::String::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")); + /* Set options, if passed */ + if(args.isSet("verbose")) importer->addFlags(Trade::ImporterFlag::Verbose); + Implementation::setOptions(*importer, "AnyImageImporter", args.value("importer-options")); + + /* Print image info, if requested. This is always done for just one + file, checked above. */ + if(args.isSet("info")) { + /* Open the file, but don't fail when an image can't be + opened */ + if(!importer->openFile(input)) { + Error() << "Cannot open file" << input; + return 3; + } + + if(!importer->image1DCount() && !importer->image2DCount() && !importer->image3DCount()) { + Debug{} << "No images found in" << input; + return 0; + } + + /* Parse everything first to avoid errors interleaved with + output. In case the images have all just a single level and + no names, write them in a compact way without listing + levels. */ + bool error = false, compact = true; + Containers::Array infos = + Trade::Implementation::imageInfo(*importer, error, compact); + + for(const Trade::Implementation::ImageInfo& info: infos) { + Debug d; + if(info.level == 0) { + if(info.size.z()) d << "3D image"; + else if(info.size.y()) d << "2D image"; + else d << "1D image"; + d << info.image << Debug::nospace << ":"; + if(!info.name.empty()) d << info.name; + if(!compact) d << Debug::newline; + } + if(!compact) d << " Level" << info.level << Debug::nospace << ":"; + if(info.compressed) d << info.compressedFormat; + else d << info.format; + if(info.size.z()) d << info.size; + else if(info.size.y()) d << info.size.xy(); + else d << Math::Vector<1, Int>(info.size.x()); + } + + return error ? 1 : 0; + } - /* Print image info, if requested */ - if(args.isSet("info")) { - /* Open the file, but don't fail when an image can't be opened */ + /* Open input file */ if(!importer->openFile(input)) { - Error() << "Cannot open file" << input; + Error{} << "Cannot open file" << input; return 3; } + /* Bail early if there's no image whatsoever. More detailed errors + with hints are provided for each dimension below. */ if(!importer->image1DCount() && !importer->image2DCount() && !importer->image3DCount()) { - Debug{} << "No images found in" << input; - return 0; + Error{} << "No images found in" << input; + return 1; } - /* Parse everything first to avoid errors interleaved with output. - In case the images have all just a single level and no names, - write them in a compact way without listing levels. */ - bool error = false, compact = true; - Containers::Array infos = - Trade::Implementation::imageInfo(*importer, error, compact); - - for(const Trade::Implementation::ImageInfo& info: infos) { - Debug d; - if(info.level == 0) { - if(info.size.z()) d << "3D image"; - else if(info.size.y()) d << "2D image"; - else d << "1D image"; - d << info.image << Debug::nospace << ":"; - if(!info.name.empty()) d << info.name; - if(!compact) d << Debug::newline; + bool imported = false; + if(dimensions == 1) { + if(!importer->image1DCount()) { + Error{} << "No 1D images found in" << input << Debug::nospace << ". Specify -D2 or -D3 for 2D or 3D image conversion."; + return 1; + } + if(image >= importer->image1DCount()) { + Error{} << "1D image number" << image << "not found in" << input << Debug::nospace << ", the file has only" << importer->image1DCount() << "1D images"; + return 1; + } + if(level >= importer->image1DLevelCount(image)) { + Error{} << "1D image" << image << "in" << input << "doesn't have a level number" << level << Debug::nospace << ", only" << importer->image1DLevelCount(image) << "levels"; + return 1; } - if(!compact) d << " Level" << info.level << Debug::nospace << ":"; - if(info.compressed) d << info.compressedFormat; - else d << info.format; - if(info.size.z()) d << info.size; - else if(info.size.y()) d << info.size.xy(); - else d << Math::Vector<1, Int>(info.size.x()); - } - return error ? 1 : 0; - } + if(Containers::Optional image1D = importer->image1D(image, level)) { + arrayAppend(images1D, std::move(*image1D)); + imported = true; + } - /* Open input file */ - if(!importer->openFile(input)) { - Error{} << "Cannot open file" << input; - return 3; - } + } else if(dimensions == 2) { + if(!importer->image2DCount()) { + Error{} << "No 2D images found in" << input << Debug::nospace << ". Specify -D1 or -D3 for 1D or 3D image conversion."; + return 1; + } + if(image >= importer->image2DCount()) { + Error{} << "2D image number" << image << "not found in" << input << Debug::nospace << ", the file has only" << importer->image2DCount() << "2D images"; + return 1; + } + if(level >= importer->image2DLevelCount(image)) { + Error{} << "2D image" << image << "in" << input << "doesn't have a level number" << level << Debug::nospace << ", only" << importer->image2DLevelCount(image) << "levels"; + return 1; + } - /* Bail early if there's no image whatsoever. More detailed errors with - hints are provided for each dimension below. */ - if(!importer->image1DCount() && !importer->image2DCount() && !importer->image3DCount()) { - Error{} << "No images found in" << input; - return 1; - } + if(Containers::Optional image2D = importer->image2D(image, level)) { + arrayAppend(images2D, std::move(*image2D)); + imported = true; + } - bool imported; - if(dimensions == 1) { - if(!importer->image1DCount()) { - Error{} << "No 1D images found in" << input << Debug::nospace << ". Specify -D2 or -D3 for 2D or 3D image conversion."; - return 1; - } - if(image >= importer->image1DCount()) { - Error{} << "1D image number" << image << "not found in" << input << Debug::nospace << ", the file has only" << importer->image1DCount() << "1D images"; - return 1; - } - if(level >= importer->image1DLevelCount(image)) { - Error{} << "1D image" << image << "in" << input << "doesn't have a level number" << level << Debug::nospace << ", only" << importer->image1DLevelCount(image) << "levels"; - return 1; - } + } else if(dimensions == 3) { + if(!importer->image3DCount()) { + Error{} << "No 3D images found in" << input << Debug::nospace << ". Specify -D1 or -D2 for 1D or 2D image conversion."; + return 1; + } + if(image >= importer->image3DCount()) { + Error{} << "3D image number" << image << "not found in" << input << Debug::nospace << ", the file has only" << importer->image3DCount() << "3D images"; + return 1; + } + if(level >= importer->image3DLevelCount(image)) { + Error{} << "3D image" << image << "in" << input << "doesn't have a level number" << level << Debug::nospace << ", only" << importer->image3DLevelCount(image) << "levels"; + return 1; + } - imported = !!(image1D = importer->image1D(image, level)); + if(Containers::Optional image3D = importer->image3D(image, level)) { + arrayAppend(images3D, std::move(*image3D)); + imported = true; + } - } else if(dimensions == 2) { - if(!importer->image2DCount()) { - Error{} << "No 2D images found in" << input << Debug::nospace << ". Specify -D1 or -D3 for 1D or 3D image conversion."; - return 1; - } - if(image >= importer->image2DCount()) { - Error{} << "2D image number" << image << "not found in" << input << Debug::nospace << ", the file has only" << importer->image2DCount() << "2D images"; - return 1; - } - if(level >= importer->image2DLevelCount(image)) { - Error{} << "2D image" << image << "in" << input << "doesn't have a level number" << level << Debug::nospace << ", only" << importer->image2DLevelCount(image) << "levels"; + } else { + Error{} << "Invalid --dimensions option:" << args.value("dimensions"); return 1; } - imported = !!(image2D = importer->image2D(image, level)); - - } else if(dimensions == 3) { - if(!importer->image3DCount()) { - Error{} << "No 3D images found in" << input << Debug::nospace << ". Specify -D1 or -D2 for 1D or 2D image conversion."; - return 1; + if(!imported) { + Error{} << "Cannot import image" << image << Debug::nospace << ":" << Debug::nospace << level << "from" << input; + return 4; } - if(image >= importer->image3DCount()) { - Error{} << "3D image number" << image << "not found in" << input << Debug::nospace << ", the file has only" << importer->image3DCount() << "3D images"; + } + } + + std::string output; + if(args.isSet("in-place")) { + /* Should have been checked in a graceful way above */ + CORRADE_INTERNAL_ASSERT(args.arrayValueCount("input") == 1); + output = args.arrayValue("input", 0); + } else output = args.value("output"); + + Int outputDimensions; + /* Not strictly needed to be an Optional, acts as a sanity check that we + don't use something that wasn't populated proparly. */ + Containers::Optional outputImage1D; + Containers::Optional outputImage2D; + Containers::Optional outputImage3D; + + /* Combine multiple layers into an image of one dimension more */ + if(args.isSet("layers")) { + if(dimensions == 1) { + if(!checkCommonFormatAndSize(args, images1D)) return 1; + + outputDimensions = 2; + if(!images1D.front().isCompressed()) { + /* Allocate a new image */ + /** @todo simplify once ImageData is able to allocate on its + own, including correct padding etc */ + const Vector2i size{images1D.front().size()[0], Int(images1D.size())}; + outputImage2D = Trade::ImageData2D{ + /* Don't want to bother with row padding, it's temporary + anyway */ + PixelStorage{}.setAlignment(1), + images1D.front().format(), + size, + Containers::Array{NoInit, size.product()*images1D.front().pixelSize()} + }; + + /* Copy the pixel data over */ + const Containers::StridedArrayView3D outputPixels = outputImage2D->mutablePixels(); + for(std::size_t i = 0; i != images1D.size(); ++i) + Utility::copy(images1D[i].pixels(), outputPixels[i]); + + } else { + Error{} << "The --layers option isn't implemented for compressed images yet."; return 1; } - if(level >= importer->image3DLevelCount(image)) { - Error{} << "3D image" << image << "in" << input << "doesn't have a level number" << level << Debug::nospace << ", only" << importer->image3DLevelCount(image) << "levels"; + + } else if(dimensions == 2) { + if(!checkCommonFormatAndSize(args, images2D)) return 1; + + outputDimensions = 3; + if(!images2D.front().isCompressed()) { + /* Allocate a new image */ + /** @todo simplify once ImageData is able to allocate on its + own, including correct padding etc */ + const Vector3i size{images2D.front().size(), Int(images2D.size())}; + outputImage3D = Trade::ImageData3D{ + /* Don't want to bother with row padding, it's temporary + anyway */ + PixelStorage{}.setAlignment(1), + images2D.front().format(), + size, + Containers::Array{NoInit, size.product()*images2D.front().pixelSize()} + }; + + /* Copy the pixel data over */ + const Containers::StridedArrayView4D outputPixels = outputImage3D->mutablePixels(); + for(std::size_t i = 0; i != images2D.size(); ++i) + Utility::copy(images2D[i].pixels(), outputPixels[i]); + + } else { + Error{} << "The --layers option isn't implemented for compressed images yet."; return 1; } - imported = !!(image3D = importer->image3D(image, level)); - - } else { - Error{} << "Invalid --dimensions option:" << args.value("dimensions"); + } else if(dimensions == 3) { + Error{} << "The --layers option can be only used with 1D and 2D inputs, not 3D"; return 1; - } - if(!imported) { - Error{} << "Cannot import image" << image << Debug::nospace << ":" << Debug::nospace << level << "from" << input; - return 4; - } - } + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); - const std::string output = args.value(args.isSet("in-place") ? "input" : "output"); + /* Single image conversion, just pass the input through */ + } else { + if(dimensions == 1) { + CORRADE_INTERNAL_ASSERT(images1D.size() == 1); + outputDimensions = 1; + outputImage1D = std::move(images1D.front()); + } else if(dimensions == 2) { + CORRADE_INTERNAL_ASSERT(images2D.size() == 1); + outputDimensions = 2; + outputImage2D = std::move(images2D.front()); + } else if(dimensions == 3) { + CORRADE_INTERNAL_ASSERT(images3D.size() == 1); + outputDimensions = 3; + outputImage3D = std::move(images3D.front()); + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + } { Debug d; @@ -384,23 +562,26 @@ key=true; configuration subgroups are delimited with /.)") d << "Writing raw image data of size"; else d << "Converting image of size"; - if(dimensions == 1) - d << image1D->size(); - else if(dimensions == 2) - d << image2D->size(); - else if(dimensions == 3) - d << image3D->size(); + if(outputDimensions == 1) + d << outputImage1D->size(); + else if(outputDimensions == 2) + d << outputImage2D->size(); + else if(outputDimensions == 3) + d << outputImage3D->size(); else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); d << "and format"; - if(dimensions == 1) { - if(image1D->isCompressed()) d << image1D->compressedFormat(); - else d << image1D->format(); - } else if(dimensions == 2) { - if(image2D->isCompressed()) d << image2D->compressedFormat(); - else d << image2D->format(); - } else if(dimensions == 3) { - if(image3D->isCompressed()) d << image3D->compressedFormat(); - else d << image3D->format(); + if(outputDimensions == 1) { + if(outputImage1D->isCompressed()) + d << outputImage1D->compressedFormat(); + else d << outputImage1D->format(); + } else if(outputDimensions == 2) { + if(outputImage2D->isCompressed()) + d << outputImage2D->compressedFormat(); + else d << outputImage2D->format(); + } else if(outputDimensions == 3) { + if(outputImage3D->isCompressed()) + d << outputImage3D->compressedFormat(); + else d << outputImage3D->format(); } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); d << "to" << output; } @@ -408,12 +589,12 @@ key=true; configuration subgroups are delimited with /.)") /* Save raw data, if requested */ if(args.value("converter") == "raw") { Containers::ArrayView data; - if(dimensions == 1) - data = image1D->data(); - else if(dimensions == 2) - data = image2D->data(); - else if(dimensions == 3) - data = image3D->data(); + if(outputDimensions == 1) + data = outputImage1D->data(); + else if(outputDimensions == 2) + data = outputImage3D->data(); + else if(outputDimensions == 3) + data = outputImage3D->data(); else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); return Utility::Directory::write(output, data) ? 0 : 1; } @@ -434,15 +615,15 @@ key=true; configuration subgroups are delimited with /.)") /* Save output file */ bool converted; - if(dimensions == 1) - converted = converter->convertToFile(*image1D, output); - else if(dimensions == 2) - converted = converter->convertToFile(*image2D, output); - else if(dimensions == 3) - converted = converter->convertToFile(*image3D, output); + if(outputDimensions == 1) + converted = converter->convertToFile(*outputImage1D, output); + else if(outputDimensions == 2) + converted = converter->convertToFile(*outputImage2D, output); + else if(outputDimensions == 3) + converted = converter->convertToFile(*outputImage3D, output); else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); if(!converted) { - Error() << "Cannot save file" << output; + Error{} << "Cannot save file" << output; return 5; } }