Browse Source

imageconverter: ability to combine multiple image layers together.

Hey, this was fun, after all!
pull/537/head
Vladimír Vondruš 5 years ago
parent
commit
b83099a8c6
  1. 3
      doc/changelog.dox
  2. 281
      src/Magnum/Trade/imageconverter.cpp

3
doc/changelog.dox

@ -208,7 +208,8 @@ See also:
are encouraged over @relativeref{Trade::AbstractImporter,setFlags()} as it are encouraged over @relativeref{Trade::AbstractImporter,setFlags()} as it
avoid accidentally clearing default flags potentially added in the future. avoid accidentally clearing default flags potentially added in the future.
- Ability to convert also 1D and 3D images with the - 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 @subsubsection changelog-latest-new-vk Vk library

281
src/Magnum/Trade/imageconverter.cpp

@ -26,8 +26,10 @@
#include <Corrade/Containers/Optional.h> #include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/GrowableArray.h> #include <Corrade/Containers/GrowableArray.h>
#include <Corrade/Containers/StaticArray.h> #include <Corrade/Containers/StaticArray.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/PluginManager/Manager.h> #include <Corrade/PluginManager/Manager.h>
#include <Corrade/Utility/Arguments.h> #include <Corrade/Utility/Arguments.h>
#include <Corrade/Utility/Algorithms.h>
#include <Corrade/Utility/ConfigurationGroup.h> #include <Corrade/Utility/ConfigurationGroup.h>
#include <Corrade/Utility/DebugStl.h> #include <Corrade/Utility/DebugStl.h>
#include <Corrade/Utility/Directory.h> #include <Corrade/Utility/Directory.h>
@ -69,7 +71,7 @@ magnum-imageconverter [-h|--help] [-I|--importer PLUGIN]
[-C|--converter PLUGIN] [--plugin-dir DIR] [-C|--converter PLUGIN] [--plugin-dir DIR]
[-i|--importer-options key=val,key2=val2,] [-i|--importer-options key=val,key2=val2,]
[-c|--converter-options key=val,key2=val2,] [-D|--dimensions N] [-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 [--] input output
@endcode @endcode
@ -92,6 +94,8 @@ Arguments:
(default: `2`) (default: `2`)
- `--image N` --- image to import (default: `0`) - `--image N` --- image to import (default: `0`)
- `--level N` --- image level 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 - `--in-place` --- overwrite the input image with the output
- `--info` --- print info about the input file and exit - `--info` --- print info about the input file and exit
- `-v`, `--verbose` --- verbose output from importer and converter plugins - `-v`, `--verbose` --- verbose output from importer and converter plugins
@ -146,9 +150,58 @@ magnum-imageconverter image.dds --converter raw data.dat
using namespace Magnum; using namespace Magnum;
namespace {
template<UnsignedInt dimensions> bool checkCommonFormat(const Utility::Arguments& args, const Containers::Array<Trade::ImageData<dimensions>>& 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<UnsignedInt dimensions> bool checkCommonFormatAndSize(const Utility::Arguments& args, const Containers::Array<Trade::ImageData<dimensions>>& images) {
if(!checkCommonFormat(args, images)) return false;
CORRADE_INTERNAL_ASSERT(!images.empty());
Math::Vector<dimensions, Int> 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) { int main(int argc, char** argv) {
Utility::Arguments args; 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") .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('I', "importer", "AnyImageImporter").setHelp("importer", "image importer plugin", "PLUGIN")
.addOption('C', "converter", "AnyImageConverter").setHelp("converter", "image converter 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('D', "dimensions", "2").setHelp("dimensions", "import and convert image of given dimensions", "N")
.addOption("image", "0").setHelp("image", "image to import", "N") .addOption("image", "0").setHelp("image", "image to import", "N")
.addOption("level", "0").setHelp("level", "image level 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("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("info").setHelp("info", "print info about the input file and exit")
.addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins")
@ -201,21 +255,39 @@ key=true; configuration subgroups are delimited with /.)")
Warning{} << "Ignoring output file for --info:" << args.value<Containers::StringView>("output"); Warning{} << "Ignoring output file for --info:" << args.value<Containers::StringView>("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<Trade::AbstractImporter> importerManager{ PluginManager::Manager<Trade::AbstractImporter> importerManager{
args.value("plugin-dir").empty() ? std::string{} : args.value("plugin-dir").empty() ? std::string{} :
Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractImporter::pluginSearchPaths()[0])}; Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractImporter::pluginSearchPaths()[0])};
const Int dimensions = args.value<Int>("dimensions");
/** @todo make them array options as well? */
const UnsignedInt image = args.value<UnsignedInt>("image");
const UnsignedInt level = args.value<UnsignedInt>("level");
Containers::Array<Trade::ImageData1D> images1D;
Containers::Array<Trade::ImageData2D> images2D;
Containers::Array<Trade::ImageData3D> 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 /* Load raw data, if requested; assume it's a tightly-packed square of
given format */ given format */
/** @todo implement image slicing and then use `--slice "0 0 w h"` to /** @todo implement image slicing and then use `--slice "0 0 w h"` to
specify non-rectangular size (and +x +y to specify padding?) */ specify non-rectangular size (and +x +y to specify padding?) */
const std::string input = args.value("input");
const Int dimensions = args.value<Int>("dimensions");
const UnsignedInt image = args.value<UnsignedInt>("image");
const UnsignedInt level = args.value<UnsignedInt>("level");
Containers::Optional<Trade::ImageData1D> image1D;
Containers::Optional<Trade::ImageData2D> image2D;
Containers::Optional<Trade::ImageData3D> image3D;
if(Utility::String::beginsWith(args.value("importer"), "raw:")) { if(Utility::String::beginsWith(args.value("importer"), "raw:")) {
if(dimensions != 2) { if(dimensions != 2) {
Error{} << "Raw data inputs can be only used for 2D images"; Error{} << "Raw data inputs can be only used for 2D images";
@ -248,7 +320,7 @@ key=true; configuration subgroups are delimited with /.)")
return 0; 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 */ /* Otherwise load it using an importer plugin */
} else { } else {
@ -262,9 +334,11 @@ key=true; configuration subgroups are delimited with /.)")
if(args.isSet("verbose")) importer->addFlags(Trade::ImporterFlag::Verbose); if(args.isSet("verbose")) importer->addFlags(Trade::ImporterFlag::Verbose);
Implementation::setOptions(*importer, "AnyImageImporter", args.value("importer-options")); Implementation::setOptions(*importer, "AnyImageImporter", args.value("importer-options"));
/* Print image info, if requested */ /* Print image info, if requested. This is always done for just one
file, checked above. */
if(args.isSet("info")) { if(args.isSet("info")) {
/* Open the file, but don't fail when an image can't be opened */ /* Open the file, but don't fail when an image can't be
opened */
if(!importer->openFile(input)) { if(!importer->openFile(input)) {
Error() << "Cannot open file" << input; Error() << "Cannot open file" << input;
return 3; return 3;
@ -275,9 +349,10 @@ key=true; configuration subgroups are delimited with /.)")
return 0; return 0;
} }
/* Parse everything first to avoid errors interleaved with output. /* Parse everything first to avoid errors interleaved with
In case the images have all just a single level and no names, output. In case the images have all just a single level and
write them in a compact way without listing levels. */ no names, write them in a compact way without listing
levels. */
bool error = false, compact = true; bool error = false, compact = true;
Containers::Array<Trade::Implementation::ImageInfo> infos = Containers::Array<Trade::Implementation::ImageInfo> infos =
Trade::Implementation::imageInfo(*importer, error, compact); Trade::Implementation::imageInfo(*importer, error, compact);
@ -309,14 +384,14 @@ key=true; configuration subgroups are delimited with /.)")
return 3; return 3;
} }
/* Bail early if there's no image whatsoever. More detailed errors with /* Bail early if there's no image whatsoever. More detailed errors
hints are provided for each dimension below. */ with hints are provided for each dimension below. */
if(!importer->image1DCount() && !importer->image2DCount() && !importer->image3DCount()) { if(!importer->image1DCount() && !importer->image2DCount() && !importer->image3DCount()) {
Error{} << "No images found in" << input; Error{} << "No images found in" << input;
return 1; return 1;
} }
bool imported; bool imported = false;
if(dimensions == 1) { if(dimensions == 1) {
if(!importer->image1DCount()) { if(!importer->image1DCount()) {
Error{} << "No 1D images found in" << input << Debug::nospace << ". Specify -D2 or -D3 for 2D or 3D image conversion."; Error{} << "No 1D images found in" << input << Debug::nospace << ". Specify -D2 or -D3 for 2D or 3D image conversion.";
@ -331,7 +406,10 @@ key=true; configuration subgroups are delimited with /.)")
return 1; return 1;
} }
imported = !!(image1D = importer->image1D(image, level)); if(Containers::Optional<Trade::ImageData1D> image1D = importer->image1D(image, level)) {
arrayAppend(images1D, std::move(*image1D));
imported = true;
}
} else if(dimensions == 2) { } else if(dimensions == 2) {
if(!importer->image2DCount()) { if(!importer->image2DCount()) {
@ -347,7 +425,10 @@ key=true; configuration subgroups are delimited with /.)")
return 1; return 1;
} }
imported = !!(image2D = importer->image2D(image, level)); if(Containers::Optional<Trade::ImageData2D> image2D = importer->image2D(image, level)) {
arrayAppend(images2D, std::move(*image2D));
imported = true;
}
} else if(dimensions == 3) { } else if(dimensions == 3) {
if(!importer->image3DCount()) { if(!importer->image3DCount()) {
@ -363,7 +444,10 @@ key=true; configuration subgroups are delimited with /.)")
return 1; return 1;
} }
imported = !!(image3D = importer->image3D(image, level)); if(Containers::Optional<Trade::ImageData3D> image3D = importer->image3D(image, level)) {
arrayAppend(images3D, std::move(*image3D));
imported = true;
}
} else { } else {
Error{} << "Invalid --dimensions option:" << args.value("dimensions"); Error{} << "Invalid --dimensions option:" << args.value("dimensions");
@ -375,8 +459,102 @@ key=true; configuration subgroups are delimited with /.)")
return 4; return 4;
} }
} }
}
const std::string output = args.value(args.isSet("in-place") ? "input" : "output"); 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<Trade::ImageData1D> outputImage1D;
Containers::Optional<Trade::ImageData2D> outputImage2D;
Containers::Optional<Trade::ImageData3D> 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<char>{NoInit, size.product()*images1D.front().pixelSize()}
};
/* Copy the pixel data over */
const Containers::StridedArrayView3D<char> 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;
}
} 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<char>{NoInit, size.product()*images2D.front().pixelSize()}
};
/* Copy the pixel data over */
const Containers::StridedArrayView4D<char> 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;
}
} else if(dimensions == 3) {
Error{} << "The --layers option can be only used with 1D and 2D inputs, not 3D";
return 1;
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
/* 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; Debug d;
@ -384,23 +562,26 @@ key=true; configuration subgroups are delimited with /.)")
d << "Writing raw image data of size"; d << "Writing raw image data of size";
else else
d << "Converting image of size"; d << "Converting image of size";
if(dimensions == 1) if(outputDimensions == 1)
d << image1D->size(); d << outputImage1D->size();
else if(dimensions == 2) else if(outputDimensions == 2)
d << image2D->size(); d << outputImage2D->size();
else if(dimensions == 3) else if(outputDimensions == 3)
d << image3D->size(); d << outputImage3D->size();
else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
d << "and format"; d << "and format";
if(dimensions == 1) { if(outputDimensions == 1) {
if(image1D->isCompressed()) d << image1D->compressedFormat(); if(outputImage1D->isCompressed())
else d << image1D->format(); d << outputImage1D->compressedFormat();
} else if(dimensions == 2) { else d << outputImage1D->format();
if(image2D->isCompressed()) d << image2D->compressedFormat(); } else if(outputDimensions == 2) {
else d << image2D->format(); if(outputImage2D->isCompressed())
} else if(dimensions == 3) { d << outputImage2D->compressedFormat();
if(image3D->isCompressed()) d << image3D->compressedFormat(); else d << outputImage2D->format();
else d << image3D->format(); } else if(outputDimensions == 3) {
if(outputImage3D->isCompressed())
d << outputImage3D->compressedFormat();
else d << outputImage3D->format();
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); } else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
d << "to" << output; d << "to" << output;
} }
@ -408,12 +589,12 @@ key=true; configuration subgroups are delimited with /.)")
/* Save raw data, if requested */ /* Save raw data, if requested */
if(args.value("converter") == "raw") { if(args.value("converter") == "raw") {
Containers::ArrayView<const char> data; Containers::ArrayView<const char> data;
if(dimensions == 1) if(outputDimensions == 1)
data = image1D->data(); data = outputImage1D->data();
else if(dimensions == 2) else if(outputDimensions == 2)
data = image2D->data(); data = outputImage3D->data();
else if(dimensions == 3) else if(outputDimensions == 3)
data = image3D->data(); data = outputImage3D->data();
else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
return Utility::Directory::write(output, data) ? 0 : 1; return Utility::Directory::write(output, data) ? 0 : 1;
} }
@ -434,15 +615,15 @@ key=true; configuration subgroups are delimited with /.)")
/* Save output file */ /* Save output file */
bool converted; bool converted;
if(dimensions == 1) if(outputDimensions == 1)
converted = converter->convertToFile(*image1D, output); converted = converter->convertToFile(*outputImage1D, output);
else if(dimensions == 2) else if(outputDimensions == 2)
converted = converter->convertToFile(*image2D, output); converted = converter->convertToFile(*outputImage2D, output);
else if(dimensions == 3) else if(outputDimensions == 3)
converted = converter->convertToFile(*image3D, output); converted = converter->convertToFile(*outputImage3D, output);
else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
if(!converted) { if(!converted) {
Error() << "Cannot save file" << output; Error{} << "Cannot save file" << output;
return 5; return 5;
} }
} }

Loading…
Cancel
Save