mirror of https://github.com/mosra/magnum.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1177 lines
57 KiB
1177 lines
57 KiB
/* |
|
This file is part of Magnum. |
|
|
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, |
|
2020, 2021, 2022 Vladimír Vondruš <mosra@centrum.cz> |
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a |
|
copy of this software and associated documentation files (the "Software"), |
|
to deal in the Software without restriction, including without limitation |
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|
and/or sell copies of the Software, and to permit persons to whom the |
|
Software is furnished to do so, subject to the following conditions: |
|
|
|
The above copyright notice and this permission notice shall be included |
|
in all copies or substantial portions of the Software. |
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
DEALINGS IN THE SOFTWARE. |
|
*/ |
|
|
|
#include <Corrade/Containers/Optional.h> |
|
#include <Corrade/Containers/GrowableArray.h> |
|
#include <Corrade/Containers/StaticArray.h> |
|
#include <Corrade/Containers/StridedArrayView.h> |
|
#include <Corrade/PluginManager/Manager.h> |
|
#include <Corrade/Utility/Arguments.h> |
|
#include <Corrade/Utility/Algorithms.h> |
|
#include <Corrade/Utility/ConfigurationGroup.h> |
|
#include <Corrade/Utility/DebugStl.h> /** @todo remove once Arguments is std::string-free */ |
|
#include <Corrade/Utility/Format.h> |
|
#include <Corrade/Utility/Path.h> |
|
|
|
#include "Magnum/ImageView.h" |
|
#include "Magnum/PixelFormat.h" |
|
#include "Magnum/Implementation/converterUtilities.h" |
|
#include "Magnum/Trade/AbstractImporter.h" |
|
#include "Magnum/Trade/AbstractImageConverter.h" |
|
#include "Magnum/Trade/ImageData.h" |
|
#include "Magnum/Trade/Implementation/converterUtilities.h" |
|
|
|
namespace Magnum { |
|
|
|
/** @page magnum-imageconverter Image conversion utility |
|
@brief Converts images of different formats |
|
|
|
@tableofcontents |
|
@m_footernavigation |
|
@m_keywords{magnum-imageconverter imageconverter} |
|
|
|
This utility is built if `MAGNUM_WITH_IMAGECONVERTER` is enabled when building |
|
Magnum. To use this utility with CMake, you need to request the |
|
`imageconverter` component of the `Magnum` package and use the |
|
`Magnum::imageconverter` target for example in a custom command: |
|
|
|
@code{.cmake} |
|
find_package(Magnum REQUIRED imageconverter) |
|
|
|
add_custom_command(OUTPUT ... COMMAND Magnum::imageconverter ...) |
|
@endcode |
|
|
|
See @ref building, @ref cmake and the @ref Trade namespace for more |
|
information. There's also a corresponding @ref magnum-sceneconverter "scene conversion utility". |
|
|
|
@section magnum-imageconverter-example Example usage |
|
|
|
Listing contents of a cubemap DDS file with mipmaps, implicitly using |
|
@relativeref{Trade,AnyImageImporter} that delegates to |
|
@relativeref{Trade,DdsImporter} or |
|
@ref file-formats "any other plugin capable of DDS import" depending on what's |
|
available: |
|
|
|
@m_class{m-code-figure} |
|
|
|
@parblock |
|
|
|
@code{.sh} |
|
magnum-imageconverter --info cubemap.dds |
|
@endcode |
|
|
|
<b></b> |
|
|
|
@m_class{m-nopad} |
|
|
|
@include imageconverter-info.ansi |
|
|
|
@endparblock |
|
|
|
Converting a JPEG file to a PNG, implicitly using |
|
@relativeref{Trade,AnyImageConverter} that delegates to |
|
@relativeref{Trade,PngImageConverter}, @relativeref{Trade,StbImageConverter} or |
|
@ref file-formats "any other plugin capable of PNG export" depending on what's |
|
available: |
|
|
|
@code{.sh} |
|
magnum-imageconverter image.jpg image.png |
|
@endcode |
|
|
|
Creating a JPEG file with 95% quality from a PNG, by setting a plugin-specific |
|
configuration option that's recognized by both |
|
@ref Trade-JpegImageConverter-configuration "JpegImageConverter" and |
|
@ref Trade-StbImageConverter-configuration "StbImageConverter": |
|
|
|
@code{.sh} |
|
magnum-imageconverter image.png image.jpg -c jpegQuality=0.95 |
|
@endcode |
|
|
|
Extracting raw (uncompressed or block-compressed) data from a DDS file for |
|
manual inspection: |
|
|
|
@code{.sh} |
|
magnum-imageconverter image.dds --converter raw data.dat |
|
@endcode |
|
|
|
Extracting an arbitrary image from a glTF file. Note that only image formats |
|
are considered by default so you have to explicitly supply a scene importer, |
|
either the generic @relativeref{Trade,AnySceneImporter} or for example directly |
|
the @relativeref{Trade,GltfImporter}. First printing a list of images to choose |
|
from: |
|
|
|
@m_class{m-code-figure} |
|
|
|
@parblock |
|
|
|
@code{.sh} |
|
magnum-imageconverter -I AnySceneImporter --info file.gltf |
|
@endcode |
|
|
|
<b></b> |
|
|
|
@m_class{m-nopad} |
|
|
|
@include imageconverter-info-gltf.ansi |
|
|
|
@endparblock |
|
|
|
@m_class{m-noindent} |
|
|
|
and then extracting the third image to a PNG file for inspection: |
|
|
|
@code{.sh} |
|
magnum-imageconverter -I AnySceneImporter --image 2 file.gltf image.png |
|
@endcode |
|
|
|
Converting a PNG file to a KTX2, block-compressing the data to BC3 using |
|
@relativeref{Trade,StbDxtImageConverter} and enabling a high-quality output. |
|
Because the plugin implements image-to-image conversion, the @relativeref{Trade,AnyImageConverter} plugin is implicitly used after it, |
|
proxying to @relativeref{Trade,KtxImageConverter} as the `*.ktx2` extension was |
|
chosen: |
|
|
|
@code{.sh} |
|
magnum-imageconverter image.png -C StbDxtImageConverter -c highQuality image.ktx2 |
|
@endcode |
|
|
|
@subsection magnum-imageconverter-example-levels-layers Dealing with image levels and layers |
|
|
|
Converting six 2D images to a 3D cube map file using @relativeref{Trade,OpenExrImageConverter}. Note the `-c envmap-cube` which the |
|
plugin needs to produce an actual cube map file, the `--` is then used to avoid |
|
`-x.exr` and others to be treated as options instead of files. On Unix shells |
|
you could also use `./-x.exr` etc. to circumvent that ambiguity. |
|
|
|
@code{.sh} |
|
magnum-imageconverter --layers -c envmap=cube -- \ |
|
+x.exr -x.exr +y.exr -y.exr +z.exr -z.exr cube.exr |
|
@endcode |
|
|
|
Creating a multi-level OpenEXR cube map file from a set of input files. Note |
|
the use of `-D3` which switches to dealing with 3D images instead of 2D: |
|
|
|
@code{.sh} |
|
magnum-imageconverter --levels -c envmap=cube -D3 \ |
|
cube-256.exr cube-128.exr cube-64.exr cube-mips.exr |
|
@endcode |
|
|
|
Extracting the second level of a +Y face (third layer) of the above cube map |
|
file again: |
|
|
|
@code{.sh} |
|
magnum-imageconverter cube-mips.exr --layer 2 --level 1 +x-128.exr |
|
@endcode |
|
|
|
@section magnum-imageconverter-usage Full usage documentation |
|
|
|
@code{.sh} |
|
magnum-imageconverter [-h|--help] [-I|--importer PLUGIN] |
|
[-C|--converter PLUGIN]... [--plugin-dir DIR] [--map] |
|
[-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 |
|
@endcode |
|
|
|
Arguments: |
|
|
|
- `input` --- input image |
|
- `output` --- output image; ignored if `--info` is present, disallowed for |
|
`--in-place` |
|
- `-h`, `--help` --- display this help message and exit |
|
- `-I`, `--importer PLUGIN` --- image importer plugin (default: |
|
@ref Trade::AnyImageImporter "AnyImageImporter") |
|
- `-C`, `--converter PLUGIN` --- image converter plugin (default: |
|
@ref Trade::AnyImageConverter "AnyImageConverter") |
|
- `--plugin-dir DIR` --- override base plugin dir |
|
- `--map` --- memory-map the input for zero-copy import (works only for |
|
standalone files) |
|
- `-i`, `--importer-options key=val,key2=val2,…` --- configuration options to |
|
pass to the importer |
|
- `-c`, `--converter-options key=val,key2=val2,…` --- configuration options |
|
to pass to the converter(s) |
|
- `-D`, `--dimensions N` --- import and convert image of given dimensions |
|
(default: `2`) |
|
- `--image N` --- image to import (default: `0`) |
|
- `--level N` --- import given image level instead of all |
|
- `--layer N` --- extract a layer into an image with one dimension less |
|
- `--layers` --- combine multiple layers into an image with one dimension |
|
more |
|
- `--levels` --- combine multiple image levels into a single file |
|
- `--in-place` --- overwrite the input image with the output |
|
- `--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 |
|
- `--profile` --- measure import and conversion time |
|
|
|
Specifying `--importer raw:<format>` will treat the input as a raw |
|
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. |
|
|
|
The `-i` / `--importer-options` and `-c` / `--converter-options` arguments |
|
accept a comma-separated list of key/value pairs to set in the importer / |
|
converter plugin configuration. If the `=` character is omitted, it's |
|
equivalent to saying `key=true`; configuration subgroups are delimited with |
|
`/`. |
|
|
|
It's possible to specify the `-C` / `--converter` option (and correspondingly |
|
also `-c` / `--converter-options`) multiple times in order to chain more |
|
converters together. All converters in the chain have to support image-to-image |
|
conversion, the last converter has to be either `raw` or support either |
|
image-to-image or image-to-file conversion. If the last converter doesn't |
|
support conversion to a file, @relativeref{Trade,AnyImageConverter} is used to |
|
save its output; if no `-C` / `--converter` is specified, |
|
@relativeref{Trade,AnyImageConverter} is used. |
|
*/ |
|
|
|
} |
|
|
|
using namespace Magnum; |
|
using namespace Containers::Literals; |
|
|
|
namespace { |
|
|
|
template<UnsignedInt dimensions> bool checkCommonFormatFlags(const Utility::Arguments& args, const Containers::Array<Trade::ImageData<dimensions>>& images) { |
|
CORRADE_INTERNAL_ASSERT(!images.isEmpty()); |
|
const bool compressed = images.front().isCompressed(); |
|
PixelFormat format{}; |
|
CompressedPixelFormat compressedFormat{}; |
|
if(!compressed) format = images.front().format(); |
|
else compressedFormat = images.front().compressedFormat(); |
|
ImageFlags<dimensions> flags = images.front().flags(); |
|
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; |
|
} |
|
if(images[i].flags() != flags) { |
|
Error{} << "Images have different flags," << args.arrayValue("input", i) << "has" << images[i].flags() << Debug::nospace << ", expected" << flags; |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
template<UnsignedInt dimensions> bool checkCommonFormatAndSize(const Utility::Arguments& args, const Containers::Array<Trade::ImageData<dimensions>>& images) { |
|
if(!checkCommonFormatFlags(args, images)) return false; |
|
|
|
CORRADE_INTERNAL_ASSERT(!images.isEmpty()); |
|
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; |
|
} |
|
|
|
template<template<UnsignedInt, class> class View, UnsignedInt dimensions> bool convertOneOrMoreImagesToFile(Trade::AbstractImageConverter& converter, const Containers::Array<Trade::ImageData<dimensions>>& outputImages, const Containers::StringView output) { |
|
Containers::Array<View<dimensions, const char>> views; |
|
arrayReserve(views, outputImages.size()); |
|
for(const Trade::ImageData<dimensions>& outputImage: outputImages) |
|
arrayAppend(views, View<dimensions, const char>{outputImage}); |
|
return converter.convertToFile(views, output); |
|
} |
|
|
|
template<UnsignedInt dimensions> bool convertOneOrMoreImagesToFile(Trade::AbstractImageConverter& converter, const Containers::Array<Trade::ImageData<dimensions>>& outputImages, const Containers::StringView output) { |
|
/* If there's just one image, convert it using the single-level API. |
|
Otherwise the multi-level entrypoint would require the plugin to support |
|
multi-level conversion, and only some file formats have that. */ |
|
if(outputImages.size() == 1) |
|
return converter.convertToFile(outputImages.front(), output); |
|
|
|
CORRADE_INTERNAL_ASSERT(!outputImages.isEmpty()); |
|
if(outputImages.front().isCompressed()) |
|
return convertOneOrMoreImagesToFile<CompressedImageView, dimensions>(converter, outputImages, output); |
|
else |
|
return convertOneOrMoreImagesToFile<ImageView, dimensions>(converter, outputImages, output); |
|
} |
|
|
|
template<UnsignedInt dimensions> bool convertImages(Trade::AbstractImageConverter& converter, Containers::Array<Trade::ImageData<dimensions>>& images) { |
|
CORRADE_INTERNAL_ASSERT(!images.isEmpty()); |
|
for(Trade::ImageData<dimensions>& image: images) { |
|
Containers::Optional<Trade::ImageData<dimensions>> output = converter.convert(image); |
|
if(!output) return false; |
|
image = *std::move(output); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
} |
|
|
|
int main(int argc, char** argv) { |
|
Utility::Arguments args; |
|
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") |
|
.addArrayOption('C', "converter").setHelp("converter", "image converter plugin(s)", "PLUGIN") |
|
.addOption("plugin-dir").setHelp("plugin-dir", "override base plugin dir", "DIR") |
|
#if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) |
|
.addBooleanOption("map").setHelp("map", "memory-map the input for zero-copy import (works only for standalone files)") |
|
#endif |
|
.addOption('i', "importer-options").setHelp("importer-options", "configuration options to pass to the importer", "key=val,key2=val2,…") |
|
.addArrayOption('c', "converter-options").setHelp("converter-options", "configuration options to pass to the converter(s)", "key=val,key2=val2,…") |
|
.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").setHelp("level", "import given image level instead of all", "N") |
|
.addOption("layer").setHelp("layer", "extract a layer into an image with one dimension less", "N") |
|
.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").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(error == Utility::Arguments::ParseError::MissingArgument && |
|
key == "output" && (args.isSet("in-place") || args.isSet("info"))) |
|
return true; |
|
|
|
/* Handle all other errors as usual */ |
|
return false; |
|
}) |
|
.setGlobalHelp(R"(Converts images of different formats. |
|
|
|
Specifying --importer raw:<format> 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. |
|
|
|
The -i / --importer-options and -c / --converter-options arguments accept a |
|
comma-separated list of key/value pairs to set in the importer / converter |
|
plugin configuration. If the = character is omitted, it's equivalent to saying |
|
key=true; configuration subgroups are delimited with /. |
|
|
|
It's possible to specify the -C / --converter option (and correspondingly also |
|
-c / --converter-options) multiple times in order to chain more converters |
|
together. All converters in the chain have to support image-to-image |
|
conversion, the last converter has to be either raw or support either |
|
image-to-image or image-to-file conversion. If the last converter doesn't |
|
support conversion to a file, AnyImageConverter is used to save its output; if |
|
no -C / --converter is specified, AnyImageConverter is used.)") |
|
.parse(argc, argv); |
|
|
|
/* Generic checks */ |
|
if(!args.value<Containers::StringView>("output").isEmpty()) { |
|
if(args.isSet("in-place")) { |
|
Error{} << "Output file shouldn't be set for --in-place:" << args.value<Containers::StringView>("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")) |
|
Warning{} << "Ignoring output file for --info:" << args.value<Containers::StringView>("output"); |
|
} |
|
|
|
/* Mutually incompatible options */ |
|
if(args.isSet("layers") && args.isSet("levels")) { |
|
Error{} << "The --layers and --levels options can't be used together. First combine layers of each level and then all levels in a second step."; |
|
return 1; |
|
} |
|
if((args.isSet("layers") || args.isSet("levels")) && args.isSet("in-place")) { |
|
Error{} << "The --layers / --levels option can't be combined with --in-place"; |
|
return 1; |
|
} |
|
if((args.isSet("layers") || args.isSet("levels")) && args.isSet("info")) { |
|
Error{} << "The --layers / --levels option can't be combined with --info"; |
|
return 1; |
|
} |
|
/* It can be combined with --levels though. This could potentially be |
|
possible to implement, but I don't see a reason, all it would do is |
|
picking Nth image from the input set and recompress it. OTOH, combining |
|
--levels and --level "works", the --level picks Nth level from each |
|
input image, although the usefulness of that is also doubtful. Why |
|
create multi-level images from images that are already multi-level? */ |
|
if(args.isSet("layers") && !args.value("layer").empty()) { |
|
Error{} << "The --layers option can't be combined with --layer."; |
|
return 1; |
|
} |
|
if(args.isSet("levels") && args.arrayValueCount("converter") && args.arrayValue("converter", args.arrayValueCount("converter") - 1) == "raw") { |
|
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) { |
|
Error{} << "Multiple input files require the --layers / --levels option to be set"; |
|
return 1; |
|
} |
|
|
|
PluginManager::Manager<Trade::AbstractImporter> importerManager{ |
|
args.value("plugin-dir").empty() ? Containers::String{} : |
|
Utility::Path::join(args.value("plugin-dir"), Trade::AbstractImporter::pluginSearchPaths().back())}; |
|
|
|
const Int dimensions = args.value<Int>("dimensions"); |
|
/** @todo make them array options as well? */ |
|
const UnsignedInt image = args.value<UnsignedInt>("image"); |
|
Containers::Optional<UnsignedInt> level; |
|
if(!args.value("level").empty()) level = args.value<UnsignedInt>("level"); |
|
#if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) |
|
Containers::Array<Containers::Array<const char, Utility::Path::MapDeleter>> mapped; |
|
#endif |
|
Containers::Array<Trade::ImageData1D> images1D; |
|
Containers::Array<Trade::ImageData2D> images2D; |
|
Containers::Array<Trade::ImageData3D> images3D; |
|
|
|
/* Wow, C++, you suck. This implicitly initializes to random shit?! */ |
|
std::chrono::high_resolution_clock::duration importTime{}; |
|
|
|
for(std::size_t i = 0, max = args.arrayValueCount("input"); i != max; ++i) { |
|
const Containers::StringView input = args.arrayValue<Containers::StringView>("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(args.value<Containers::StringView>("importer").hasPrefix("raw:"_s)) { |
|
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<PixelFormat>::fromString(args.value("importer").substr(4), {}); |
|
if(format == PixelFormat{}) { |
|
Error{} << "Invalid raw pixel format" << args.value("importer"); |
|
return 4; |
|
} |
|
const UnsignedInt pixelSize = pixelFormatSize(format); |
|
|
|
/* Read the file or map it if requested */ |
|
Containers::Array<char> data; |
|
#if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) |
|
if(args.isSet("map")) { |
|
arrayAppend(mapped, InPlaceInit); |
|
|
|
Trade::Implementation::Duration d{importTime}; |
|
Containers::Optional<Containers::Array<const char, Utility::Path::MapDeleter>> mappedMaybe = Utility::Path::mapRead(input); |
|
if(!mappedMaybe) { |
|
Error() << "Cannot memory-map file" << input; |
|
return 3; |
|
} |
|
|
|
/* Fake a mutable array with a non-owning deleter to have the |
|
same type as from Path::read(). The actual memory is owned |
|
by the `mapped` array. */ |
|
mapped.back() = *std::move(mappedMaybe); |
|
data = Containers::Array<char>{const_cast<char*>(mapped.back().data()), mapped.back().size(), [](char*, std::size_t){}}; |
|
} else |
|
#endif |
|
{ |
|
Trade::Implementation::Duration d{importTime}; |
|
Containers::Optional<Containers::Array<char>> dataMaybe = Utility::Path::read(input); |
|
if(!dataMaybe) { |
|
Error{} << "Cannot read file" << input; |
|
return 3; |
|
} |
|
|
|
data = *std::move(dataMaybe); |
|
} |
|
|
|
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:" << format << Vector2i{side}; |
|
|
|
if(args.isSet("profile")) { |
|
Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast<std::chrono::milliseconds>(importTime).count())/1.0e3f << "seconds"; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
arrayAppend(images2D, InPlaceInit, format, Vector2i{side}, std::move(data)); |
|
|
|
/* Otherwise load it using an importer plugin */ |
|
} else { |
|
Containers::Pointer<Trade::AbstractImporter> 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")); |
|
|
|
/* Open the file or map it if requested */ |
|
#if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) |
|
if(args.isSet("map")) { |
|
arrayAppend(mapped, InPlaceInit); |
|
|
|
Trade::Implementation::Duration d{importTime}; |
|
Containers::Optional<Containers::Array<const char, Utility::Path::MapDeleter>> mappedMaybe = Utility::Path::mapRead(input); |
|
if(!mappedMaybe || !importer->openMemory(*mappedMaybe)) { |
|
Error() << "Cannot memory-map file" << input; |
|
return 3; |
|
} |
|
|
|
mapped.back() = *std::move(mappedMaybe); |
|
} else |
|
#endif |
|
{ |
|
Trade::Implementation::Duration d{importTime}; |
|
if(!importer->openFile(input)) { |
|
Error{} << "Cannot open file" << input; |
|
return 3; |
|
} |
|
} |
|
|
|
/* Print image info, if requested. This is always done for just one |
|
file, checked above. */ |
|
if(args.isSet("info")) { |
|
/* Don't fail when there's no image -- we could be asking for |
|
info on a scene file without images, after all */ |
|
if(!importer->image1DCount() && !importer->image2DCount() && !importer->image3DCount()) { |
|
Debug{} << "No images found in" << input; |
|
return 0; |
|
} |
|
|
|
/* Parse everything first to avoid errors interleaved with |
|
output */ |
|
bool error = false; |
|
Containers::Array<Trade::Implementation::ImageInfo> infos = |
|
Trade::Implementation::imageInfo(*importer, error, importTime); |
|
|
|
/* Colored output. Enable only if a TTY. */ |
|
Debug::Flags useColor; |
|
if(args.value("color") == "on") |
|
useColor = Debug::Flags{}; |
|
else if(args.value("color") == "off") |
|
useColor = Debug::Flag::DisableColors; |
|
else |
|
useColor = Debug::isTty() ? Debug::Flags{} : Debug::Flag::DisableColors; |
|
|
|
std::size_t totalImageDataSize = 0; |
|
for(const Trade::Implementation::ImageInfo& info: infos) { |
|
Debug d{useColor}; |
|
if(info.level == 0) { |
|
d << Debug::boldColor(Debug::Color::White); |
|
if(info.size.z()) d << "3D image"; |
|
else if(info.size.y()) d << "2D image"; |
|
else d << "1D image"; |
|
d << info.image << Debug::nospace << ":" |
|
<< Debug::resetColor; |
|
if(info.name) d << Debug::boldColor(Debug::Color::Yellow) |
|
<< info.name << Debug::resetColor; |
|
d << Debug::newline; |
|
} |
|
d << " Level" << info.level << Debug::nospace << ":"; |
|
if(info.flags.one) { |
|
d << Debug::packed << Debug::color(Debug::Color::Cyan); |
|
if(info.size.z()) d << info.flags.three; |
|
else if(info.size.y()) d << info.flags.two; |
|
else d << info.flags.one; |
|
d << Debug::resetColor; |
|
} |
|
d << Debug::packed; |
|
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()); |
|
d << Debug::color(Debug::Color::Blue) << "@" << Debug::resetColor; |
|
d << Debug::packed; |
|
if(info.compressed) d << Debug::color(Debug::Color::Yellow) << info.compressedFormat; |
|
else d << Debug::color(Debug::Color::Cyan) << info.format; |
|
d << Debug::resetColor << "(" << Debug::nospace << Utility::format("{:.1f}", info.dataSize/1024.0f) << "kB"; |
|
if(info.dataFlags != (Trade::DataFlag::Owned|Trade::DataFlag::Mutable)) |
|
d << Debug::nospace << "," << Debug::packed |
|
<< Debug::color(Debug::Color::Green) |
|
<< info.dataFlags << Debug::resetColor; |
|
d << Debug::nospace << ")"; |
|
|
|
totalImageDataSize += info.dataSize; |
|
} |
|
if(!infos.isEmpty()) |
|
Debug{} << "Total image data size:" << Utility::format("{:.1f}", totalImageDataSize/1024.0f) << "kB"; |
|
|
|
if(args.isSet("profile")) { |
|
Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast<std::chrono::milliseconds>(importTime).count())/1.0e3f << "seconds"; |
|
} |
|
|
|
return error ? 1 : 0; |
|
} |
|
|
|
/* 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; |
|
} |
|
|
|
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; |
|
} |
|
|
|
/* Import all levels of the input or just one if specified */ |
|
UnsignedInt minLevel, maxLevel; |
|
if(level) { |
|
minLevel = *level; |
|
maxLevel = *level + 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 { |
|
minLevel = 0; |
|
maxLevel = importer->image1DLevelCount(image); |
|
if(maxLevel > 1 && (args.isSet("layers") || args.isSet("levels") || (args.arrayValueCount("converter") && args.arrayValue("converter", args.arrayValueCount("converter") - 1) == "raw"))) { |
|
Error{} << "Cannot use --layers / --levels or raw output with multi-level input images. Specify --level N to extract just one level from each."; |
|
return 1; |
|
} |
|
} |
|
for(; minLevel != maxLevel; ++minLevel) { |
|
if(Containers::Optional<Trade::ImageData1D> image1D = importer->image1D(image, minLevel)) { |
|
/* The --layer option is only for 2D/3D, not checking |
|
any bounds here. If the option is present, the |
|
extraction code below will fail. */ |
|
arrayAppend(images1D, std::move(*image1D)); |
|
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; |
|
} |
|
|
|
/* Import all levels of the input or just one if specified */ |
|
UnsignedInt minLevel, maxLevel; |
|
if(level) { |
|
minLevel = *level; |
|
maxLevel = *level + 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; |
|
} |
|
} else { |
|
minLevel = 0; |
|
maxLevel = importer->image2DLevelCount(image); |
|
if(maxLevel > 1 && (args.isSet("layers") || args.isSet("levels") || (args.arrayValueCount("converter") && args.arrayValue("converter", args.arrayValueCount("converter") - 1) == "raw"))) { |
|
Error{} << "Cannot use --layers / --levels or raw output with multi-level input images. Specify --level N to extract just one level from each."; |
|
return 1; |
|
} |
|
} |
|
for(; minLevel != maxLevel; ++minLevel) { |
|
if(Containers::Optional<Trade::ImageData2D> image2D = importer->image2D(image, minLevel)) { |
|
/* Check bounds for the --layer option here, as we |
|
won't have the filename etc. later */ |
|
if(!args.value("layer").empty() && args.value<Int>("layer") >= image2D->size().y()) { |
|
Error{} << "2D image" << image << Debug::nospace << ":" << Debug::nospace << minLevel << "in" << input << "doesn't have a layer number" << args.value<Int>("layer") << Debug::nospace << ", only" << image2D->size().y() << "layers"; |
|
return 1; |
|
} |
|
|
|
arrayAppend(images2D, std::move(*image2D)); |
|
imported = true; |
|
} |
|
} |
|
|
|
} 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; |
|
} |
|
|
|
/* Import all levels of the input or just one if specified */ |
|
UnsignedInt minLevel, maxLevel; |
|
if(level) { |
|
minLevel = *level; |
|
maxLevel = *level + 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; |
|
} |
|
} else { |
|
minLevel = 0; |
|
maxLevel = importer->image3DLevelCount(image); |
|
if(maxLevel > 1 && (args.isSet("layers") || args.isSet("levels") || (args.arrayValueCount("converter") && args.arrayValue("converter", args.arrayValueCount("converter") - 1) == "raw"))) { |
|
Error{} << "Cannot use --layers / --levels or raw output with multi-level input images. Specify --level N to extract just one level from each."; |
|
return 1; |
|
} |
|
} |
|
for(; minLevel != maxLevel; ++minLevel) { |
|
if(Containers::Optional<Trade::ImageData3D> image3D = importer->image3D(image, minLevel)) { |
|
/* Check bounds for the --layer option here, as we |
|
won't have the filename etc. later */ |
|
if(!args.value("layer").empty() && args.value<Int>("layer") >= image3D->size().z()) { |
|
Error{} << "3D image" << image << Debug::nospace << ":" << Debug::nospace << minLevel << "in" << input << "doesn't have a layer number" << args.value<Int>("layer") << Debug::nospace << ", only" << image3D->size().z() << "layers"; |
|
return 1; |
|
} |
|
|
|
arrayAppend(images3D, std::move(*image3D)); |
|
imported = true; |
|
} |
|
} |
|
|
|
} else { |
|
Error{} << "Invalid --dimensions option:" << args.value("dimensions"); |
|
return 1; |
|
} |
|
|
|
if(!imported) { |
|
Error{} << "Cannot import image" << image << Debug::nospace << ":" << Debug::nospace << level << "from" << input; |
|
return 4; |
|
} |
|
} |
|
} |
|
|
|
/* Wow, C++, you suck. This implicitly initializes to random shit?! */ |
|
std::chrono::high_resolution_clock::duration conversionTime{}; |
|
|
|
Containers::StringView 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<Containers::StringView>("input", 0); |
|
} else output = args.value<Containers::StringView>("output"); |
|
|
|
Int outputDimensions; |
|
Containers::Array<Trade::ImageData1D> outputImages1D; |
|
Containers::Array<Trade::ImageData2D> outputImages2D; |
|
Containers::Array<Trade::ImageData3D> outputImages3D; |
|
|
|
/* Combine multiple layers into an image of one dimension more */ |
|
if(args.isSet("layers")) { |
|
/* To include allocation + copy costs in the output */ |
|
Trade::Implementation::Duration d{conversionTime}; |
|
|
|
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())}; |
|
arrayAppend(outputImages2D, InPlaceInit, |
|
/* 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 = outputImages2D.front().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())}; |
|
arrayAppend(outputImages3D, InPlaceInit, |
|
/* 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 = outputImages3D.front().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(); |
|
|
|
/* Extracting a layer, inverse of the above */ |
|
} else if(!args.value("layer").empty()) { |
|
const Int layer = args.value<Int>("layer"); |
|
|
|
if(dimensions == 1) { |
|
Error{} << "The --layer option can be only used with 2D and 3D inputs, not 1D"; |
|
return 1; |
|
|
|
} else if(dimensions == 2) { |
|
outputDimensions = 1; |
|
|
|
/* There can be multiple input levels, and a layer should get |
|
extracted from each level, forming a multi-level image again */ |
|
if(!checkCommonFormatFlags(args, images2D)) return 1; |
|
if(!images2D.front().isCompressed()) { |
|
for(std::size_t i = 0; i != images2D.size(); ++i) { |
|
/* Diagnostic printed in the import loop above, as here we |
|
don't have the filename etc. anymore */ |
|
CORRADE_INTERNAL_ASSERT(layer < images2D[i].size().y()); |
|
|
|
/* Copy the layer to a newly alllocated image */ |
|
/** @todo if the GL-inspired PixelStorage API wasn't CRAP, |
|
we could just reuse the original memory and slice it. |
|
But because Y skip is ignored for 1D images, it just |
|
won't work. Rework once it's in a better shape. */ |
|
Trade::ImageData1D copy{PixelStorage{}.setAlignment(1), |
|
images2D[i].format(), images2D[i].formatExtra(), |
|
images2D[i].pixelSize(), images2D[i].size().x(), |
|
Containers::Array<char>{NoInit, std::size_t(images2D[i].size().x()*images2D[i].pixelSize())}}; |
|
Utility::copy(images2D[i].pixels()[layer], copy.mutablePixels()); |
|
arrayAppend(outputImages1D, std::move(copy)); |
|
} |
|
|
|
} else { |
|
Error{} << "The --layer option isn't implemented for compressed images yet."; |
|
return 1; |
|
} |
|
|
|
} else if(dimensions == 3) { |
|
outputDimensions = 2; |
|
|
|
/* There can be multiple input levels, and a layer should get |
|
extracted from each level, forming a multi-level image again */ |
|
if(!checkCommonFormatFlags(args, images3D)) return 1; |
|
if(!images3D.front().isCompressed()) { |
|
for(std::size_t i = 0; i != images3D.size(); ++i) { |
|
/* Diagnostic printed in the import loop above, as here we |
|
don't have the filename etc. anymore */ |
|
CORRADE_INTERNAL_ASSERT(layer < images3D[i].size().z()); |
|
|
|
/* Copy the layer to a newly alllocated image */ |
|
/** @todo if the GL-inspired PixelStorage API wasn't CRAP, |
|
we could just reuse the original memory and slice it. |
|
But because Y skip is ignored for 1D images, it just |
|
won't work. Rework once it's in a better shape. */ |
|
Trade::ImageData2D copy{PixelStorage{}.setAlignment(1), |
|
images3D[i].format(), images3D[i].formatExtra(), |
|
images3D[i].pixelSize(), images3D[i].size().xy(), |
|
Containers::Array<char>{NoInit, std::size_t(images3D[i].size().xy().product()*images3D[i].pixelSize())}}; |
|
Utility::copy(images3D[i].pixels()[layer], copy.mutablePixels()); |
|
arrayAppend(outputImages2D, std::move(copy)); |
|
} |
|
|
|
} else { |
|
Error{} << "The --layer option isn't implemented for compressed images yet."; |
|
return 1; |
|
} |
|
|
|
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
|
|
/* Single-image (potentially multi-level) conversion, verify that all have |
|
the same format and pass the input through. This happens either if |
|
--levels is set or if the (single) input image is multi-level. */ |
|
} else { |
|
if(dimensions == 1) { |
|
if(!checkCommonFormatFlags(args, images1D)) return 1; |
|
outputDimensions = 1; |
|
outputImages1D = std::move(images1D); |
|
} else if(dimensions == 2) { |
|
if(!checkCommonFormatFlags(args, images2D)) return 1; |
|
outputDimensions = 2; |
|
outputImages2D = std::move(images2D); |
|
} else if(dimensions == 3) { |
|
if(!checkCommonFormatFlags(args, images3D)) return 1; |
|
outputDimensions = 3; |
|
outputImages3D = std::move(images3D); |
|
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
} |
|
|
|
const bool outputIsMultiLevel = |
|
outputImages1D.size() > 1 || |
|
outputImages2D.size() > 1 || |
|
outputImages3D.size() > 1; |
|
|
|
PluginManager::Manager<Trade::AbstractImageConverter> converterManager{ |
|
args.value("plugin-dir").empty() ? Containers::String{} : |
|
Utility::Path::join(args.value("plugin-dir"), Trade::AbstractImageConverter::pluginSearchPaths().back())}; |
|
|
|
/* Assume there's always one passed --converter option less, and the last |
|
is implicitly AnyImageConverter. All converters except the last one are |
|
expected to support ConvertMesh and the mesh is "piped" from one to the |
|
other. If the last converter supports ConvertMeshToFile instead of |
|
ConvertMesh, it's used instead of the last implicit AnySceneConverter. */ |
|
for(std::size_t i = 0, converterCount = args.arrayValueCount("converter"); i <= converterCount; ++i) { |
|
const Containers::StringView converterName = i == converterCount ? |
|
"AnyImageConverter"_s : args.arrayValue<Containers::StringView>("converter", i); |
|
|
|
const bool outputIsCompressed = |
|
(outputDimensions == 1 && outputImages1D.front().isCompressed()) || |
|
(outputDimensions == 2 && outputImages2D.front().isCompressed()) || |
|
(outputDimensions == 3 && outputImages3D.front().isCompressed()); |
|
|
|
/* Load converter plugin if a raw conversion is not requested */ |
|
Containers::Pointer<Trade::AbstractImageConverter> converter; |
|
if(converterName != "raw"_s) { |
|
if(!(converter = converterManager.loadAndInstantiate(converterName))) { |
|
Debug{} << "Available converter plugins:" << ", "_s.join(converterManager.aliasList()); |
|
return 2; |
|
} |
|
|
|
/* Set options, if passed */ |
|
if(args.isSet("verbose")) converter->addFlags(Trade::ImageConverterFlag::Verbose); |
|
if(i < args.arrayValueCount("converter-options")) |
|
Implementation::setOptions(*converter, "AnyImageConverter", args.arrayValue("converter-options", i)); |
|
} |
|
|
|
/* This is the last --converter (a raw output, a file-capable converter |
|
or the implicit AnyImageConverter at the end), output to a file and |
|
exit the loop */ |
|
if(i + 1 >= converterCount && (converterName == "raw"_s || (converter->features() & ( |
|
Trade::ImageConverterFeature::Convert1DToFile| |
|
Trade::ImageConverterFeature::Convert2DToFile| |
|
Trade::ImageConverterFeature::Convert3DToFile| |
|
Trade::ImageConverterFeature::ConvertCompressed1DToFile| |
|
Trade::ImageConverterFeature::ConvertCompressed2DToFile| |
|
Trade::ImageConverterFeature::ConvertCompressed3DToFile)))) |
|
{ |
|
/* Decide what converter feature we should look for for given |
|
dimension count. This has to be redone each iteration, as a |
|
converted could have converted an uncompressed image to a |
|
compressed one and vice versa. */ |
|
if(converterName != "raw"_s) { |
|
Trade::ImageConverterFeatures expectedFeatures; |
|
if(outputDimensions == 1) { |
|
expectedFeatures = outputIsCompressed ? |
|
Trade::ImageConverterFeature::ConvertCompressed1DToFile : |
|
Trade::ImageConverterFeature::Convert1DToFile; |
|
} else if(outputDimensions == 2) { |
|
expectedFeatures = outputIsCompressed ? |
|
Trade::ImageConverterFeature::ConvertCompressed2DToFile : |
|
Trade::ImageConverterFeature::Convert2DToFile; |
|
} else if(outputDimensions == 3) { |
|
expectedFeatures = outputIsCompressed ? |
|
Trade::ImageConverterFeature::ConvertCompressed3DToFile : |
|
Trade::ImageConverterFeature::Convert3DToFile; |
|
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
if(outputIsMultiLevel) |
|
expectedFeatures |= Trade::ImageConverterFeature::Levels; |
|
if(!(converter->features() >= expectedFeatures)) { |
|
Error err; |
|
err << converterName << "doesn't support"; |
|
if(outputIsMultiLevel) |
|
err << "multi-level"; |
|
if(outputIsCompressed) |
|
err << "compressed"; |
|
err << outputDimensions << Debug::nospace << "D image to file conversion, only" << converter->features(); |
|
return 6; |
|
} |
|
} |
|
|
|
if(args.isSet("verbose")) { |
|
Debug d; |
|
if(converterName == "raw") |
|
d << "Writing raw image data of size"; |
|
else |
|
d << "Saving output of size"; |
|
d << Debug::packed; |
|
if(outputDimensions == 1) { |
|
d << outputImages1D.front().size(); |
|
if(outputImages1D.size() > 1) |
|
d << "(and" << outputImages1D.size() - 1 << "more levels)"; |
|
} else if(outputDimensions == 2) { |
|
d << outputImages2D.front().size(); |
|
if(outputImages2D.size() > 1) |
|
d << "(and" << outputImages2D.size() - 1 << "more levels)"; |
|
} else if(outputDimensions == 3) { |
|
d << outputImages3D.front().size(); |
|
if(outputImages3D.size() > 1) |
|
d << "(and" << outputImages3D.size() - 1 << "more levels)"; |
|
} |
|
else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
d << "and" << (outputIsCompressed ? "compressed format" : "format") << Debug::packed; |
|
if(outputDimensions == 1) { |
|
if(outputImages1D.front().isCompressed()) |
|
d << outputImages1D.front().compressedFormat(); |
|
else d << outputImages1D.front().format(); |
|
} else if(outputDimensions == 2) { |
|
if(outputImages2D.front().isCompressed()) |
|
d << outputImages2D.front().compressedFormat(); |
|
else d << outputImages2D.front().format(); |
|
} else if(outputDimensions == 3) { |
|
if(outputImages3D.front().isCompressed()) |
|
d << outputImages3D.front().compressedFormat(); |
|
else d << outputImages3D.front().format(); |
|
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
|
|
if(converterName != "raw") |
|
d << "with" << converterName; |
|
d << Debug::nospace << "..."; |
|
} |
|
|
|
/* Save raw data, if requested. Only for single-level images as the |
|
data layout would be messed up otherwise. */ |
|
if(converterName == "raw") { |
|
Containers::ArrayView<const char> data; |
|
if(outputDimensions == 1) { |
|
CORRADE_INTERNAL_ASSERT(outputImages1D.size() == 1); |
|
data = outputImages1D.front().data(); |
|
} else if(outputDimensions == 2) { |
|
CORRADE_INTERNAL_ASSERT(outputImages2D.size() == 1); |
|
data = outputImages2D.front().data(); |
|
} else if(outputDimensions == 3) { |
|
CORRADE_INTERNAL_ASSERT(outputImages3D.size() == 1); |
|
data = outputImages3D.front().data(); |
|
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
|
|
{ |
|
Trade::Implementation::Duration d{conversionTime}; |
|
if(!Utility::Path::write(output, data)) return 1; |
|
} |
|
|
|
/* Convert to a file */ |
|
} else { |
|
bool converted; |
|
Trade::Implementation::Duration d{conversionTime}; |
|
if(outputDimensions == 1) |
|
converted = convertOneOrMoreImagesToFile(*converter, outputImages1D, output); |
|
else if(outputDimensions == 2) |
|
converted = convertOneOrMoreImagesToFile(*converter, outputImages2D, output); |
|
else if(outputDimensions == 3) |
|
converted = convertOneOrMoreImagesToFile(*converter, outputImages3D, output); |
|
else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
if(!converted) { |
|
Error{} << "Cannot save file" << output; |
|
return 5; |
|
} |
|
} |
|
|
|
break; |
|
|
|
/* This is not the last converter, expect that it's capable of |
|
image-to-image conversion */ |
|
} else { |
|
if(converterName == "raw"_s) { |
|
Error{} << "Only the very last --converter can be raw"; |
|
return 1; |
|
} |
|
|
|
CORRADE_INTERNAL_ASSERT(i < converterCount); |
|
if(converterCount > 1 && args.isSet("verbose")) |
|
Debug{} << "Processing (" << Debug::nospace << (i+1) << Debug::nospace << "/" << Debug::nospace << converterCount << Debug::nospace << ") with" << converterName << Debug::nospace << "..."; |
|
|
|
/* Decide what converter feature we should look for for given |
|
dimension count. This has to be redone each iteration, as a |
|
converted could have converted an uncompressed image to a |
|
compressed one and vice versa. */ |
|
Trade::ImageConverterFeature expectedFeature; |
|
if(outputDimensions == 1) { |
|
expectedFeature = outputIsCompressed ? |
|
Trade::ImageConverterFeature::ConvertCompressed1D : |
|
Trade::ImageConverterFeature::Convert1D; |
|
} else if(outputDimensions == 2) { |
|
expectedFeature = outputIsCompressed ? |
|
Trade::ImageConverterFeature::ConvertCompressed2D : |
|
Trade::ImageConverterFeature::Convert2D; |
|
} else if(outputDimensions == 3) { |
|
expectedFeature = outputIsCompressed ? |
|
Trade::ImageConverterFeature::ConvertCompressed3D : |
|
Trade::ImageConverterFeature::Convert3D; |
|
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
if(!(converter->features() >= expectedFeature)) { |
|
Error err; |
|
err << converterName << "doesn't support"; |
|
if(outputIsCompressed) |
|
err << "compressed"; |
|
err << outputDimensions << Debug::nospace << "D image conversion, only" << converter->features(); |
|
return 6; |
|
} |
|
|
|
bool converted; |
|
Trade::Implementation::Duration d{conversionTime}; |
|
if(outputDimensions == 1) |
|
converted = convertImages(*converter, outputImages1D); |
|
else if(outputDimensions == 2) |
|
converted = convertImages(*converter, outputImages2D); |
|
else if(outputDimensions == 3) |
|
converted = convertImages(*converter, outputImages3D); |
|
else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); |
|
if(!converted) { |
|
Error{} << converterName << "cannot convert the image"; |
|
return 5; |
|
} |
|
} |
|
} |
|
|
|
if(args.isSet("profile")) { |
|
Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast<std::chrono::milliseconds>(importTime).count())/1.0e3f << "seconds, conversion" |
|
<< UnsignedInt(std::chrono::duration_cast<std::chrono::milliseconds>(conversionTime).count())/1.0e3f << "seconds"; |
|
} |
|
}
|
|
|