diff --git a/doc/changelog.dox b/doc/changelog.dox index f36c4a157..e972472d2 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -659,6 +659,8 @@ See also: with filtering along Z or if it's a 2D array with discrete slices. - @relativeref{Trade,TgaImporter} now recognizes and skips TGA 2 file footers instead of treating them as actual image data +- @relativeref{Trade,TgaImageConverter} now implements RLE for smaller output + size - @ref magnum-imageconverter "magnum-imageconverter" has a new `--in-place` option for converting images in-place - In order to reduce the amount of exported symbols, a single no-op diff --git a/src/MagnumPlugins/TgaImageConverter/Test/TgaImageConverterTest.cpp b/src/MagnumPlugins/TgaImageConverter/Test/TgaImageConverterTest.cpp index 09276f112..180fd8a1e 100644 --- a/src/MagnumPlugins/TgaImageConverter/Test/TgaImageConverterTest.cpp +++ b/src/MagnumPlugins/TgaImageConverter/Test/TgaImageConverterTest.cpp @@ -30,15 +30,19 @@ #include #include #include +#include +#include #include /** @todo remove once Debug is stream-free */ #include #include #include "Magnum/ImageView.h" #include "Magnum/PixelFormat.h" +#include "Magnum/Math/Color.h" #include "Magnum/Trade/ImageData.h" #include "Magnum/Trade/AbstractImageConverter.h" #include "Magnum/Trade/AbstractImporter.h" +#include "MagnumPlugins/TgaImporter/TgaHeader.h" #include "configure.h" @@ -49,9 +53,14 @@ struct TgaImageConverterTest: TestSuite::Tester { void wrongFormat(); - void rgb(); - void rgba(); - void r(); + void uncompressedRgb(); + void uncompressedRgba(); + void uncompressedR(); + + void rle(); + void rleRgb(); + void rleRgba(); + void rleDisabled(); void unsupportedMetadata(); @@ -60,6 +69,8 @@ struct TgaImageConverterTest: TestSuite::Tester { PluginManager::Manager _importerManager{"nonexistent"}; }; +using namespace Math::Literals; + constexpr struct { const char* name; ImageConverterFlags flags; @@ -72,6 +83,168 @@ constexpr struct { "Trade::TgaImageConverter::convertToData(): converting from RGBA to BGRA\n"} }; +const struct { + TestSuite::TestCaseDescriptionSourceLocation name; + Containers::Array data; + Containers::Array expected; + Int width; + Containers::Optional rleAcrossScanlines; +} RleData[]{ + {"single repeat run", {InPlaceInit, { + 3, 3, 3, 3, 3 + }}, {InPlaceInit, { + 0x80|4, 3 + }}, 5, {}}, + {"single sequence run", {InPlaceInit, { + 2, 7, 6, 5, 4, 37 + }}, {InPlaceInit, { + 0x00|5, 2, 7, 6, 5, 4, 37 + }}, 6, {}}, + {"1x1 pixel", {InPlaceInit, { + 2 + }}, {InPlaceInit, { + 0x00|0, 2 + }}, 1, {}}, + {"two repeats", {InPlaceInit, { + 1, 1, 1, 2, 2, 2, 2, 2 + }}, {InPlaceInit, { + 0x80|2, 1, 0x80|4, 2 + }}, 8, {}}, + {"sequence after a repeat", {InPlaceInit, { + 2, 2, 2, 3, 4, 5, 76 + }}, {InPlaceInit, { + 0x80|2, 2, 0x00|3, 3, 4, 5, 76 + }}, 7, {}}, + {"repeat after a sequence", {InPlaceInit, { + 3, 4, 5, 76, 2, 2, 2 + }}, {InPlaceInit, { + 0x00|3, 3, 4, 5, 76, 0x80|2, 2 + }}, 7, {}}, + {"repeat after a single different pixel", {InPlaceInit, { + 76, 2, 2 + }}, {InPlaceInit, { + 0x00|0, 76, 0x80|1, 2 + }}, 3, {}}, + {"single different pixel after a repeat", {InPlaceInit, { + 2, 2, 76 + }}, {InPlaceInit, { + 0x80|1, 2, 0x00|0, 76 + }}, 3, {}}, + {"repeat across a scanline", {InPlaceInit, { + 2, 4, 4, + 4, 4, 5 + }}, {InPlaceInit, { + 0x00|0, 2, 0x80|1, 4, 0x80|1, 4, 0x00|0, 5 + }}, 3, {}}, + {"repeat across a scanline, single pixel before", {InPlaceInit, { + 2, 3, 4, + 4, 4, 5 + }}, {InPlaceInit, { + /* Whole first line encoded as a sequence */ + 0x00|2, 2, 3, 4, + 0x80|1, 4, 0x00|0, 5 + }}, 3, {}}, + {"repeat across a scanline, single pixel after", {InPlaceInit, { + 2, 4, 4, + 4, 3, 5 + }}, {InPlaceInit, { + 0x00|0, 2, 0x80|1, 4, + /* Whole second line encoded as a sequence */ + 0x00|2, 4, 3, 5 + }}, 3, {}}, + {"repeat across a scanline, non-strict", {InPlaceInit, { + 2, 4, 4, + 4, 4, 5 + }}, {InPlaceInit, { + 0x00|0, 2, 0x80|3, 4, 0x00|0, 5 + }}, 3, true}, + {"sequence across a scanline", {InPlaceInit, { + 2, 2, 3, 4, + 5, 6, 7, 7 + }}, {InPlaceInit, { + 0x80|1, 2, 0x00|1, 3, 4, 0x00|1, 5, 6, 0x80|1, 7 + }}, 4, {}}, + {"sequence across a scanline, single pixel before", {InPlaceInit, { + 2, 2, 2, 4, + 5, 6, 7, 7 + }}, {InPlaceInit, { + 0x80|2, 2, 0x00|0, 4, 0x00|1, 5, 6, 0x80|1, 7 + }}, 4, {}}, + {"sequence across a scanline, single pixel after", {InPlaceInit, { + 2, 2, 3, 4, + 5, 7, 7, 7 + }}, {InPlaceInit, { + 0x80|1, 2, 0x00|1, 3, 4, 0x00|0, 5, 0x80|2, 7 + }}, 4, {}}, + {"sequence across a scanline, non-strict", {InPlaceInit, { + 2, 2, 3, 4, + 5, 6, 7, 7 + }}, {InPlaceInit, { + 0x80|1, 2, 0x00|3, 3, 4, 5, 6, 0x80|1, 7 + }}, 4, true}, + {"repeat & sequence across multiple scanlines, non-strict", {InPlaceInit, { + 2, 2, 2, + 2, 2, 2, + 2, 3, 4, + 5, 6, 7, + 8, 9, 0, + 1, 2, 3 + }}, {InPlaceInit, { + 0x80|6, 2, 0x00|10, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3 + }}, 3, true}, + {"repeat overflow", {InPlaceInit, { + /* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 16 */ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, + 6, 6 + }}, {InPlaceInit, { + 0x80|127, 7, 0x80|30, 7, 0x80|2, 6 + }}, 128 + 31 + 3, {}}, + {"sequence overflow", {InPlaceInit, { + /* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 16 */ + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 6, 6 + }}, {InPlaceInit, { + 0x00|127, + /* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 16 */ + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 0x00|30, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, + 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, + 0x80|2, + 6 + }}, 128 + 31 + 3, {}}, +}; + const struct { const char* name; ImageFlags2D flags; @@ -85,11 +258,18 @@ TgaImageConverterTest::TgaImageConverterTest() { addTests({&TgaImageConverterTest::wrongFormat}); addInstancedTests({ - &TgaImageConverterTest::rgb, - &TgaImageConverterTest::rgba}, + &TgaImageConverterTest::uncompressedRgb, + &TgaImageConverterTest::uncompressedRgba}, Containers::arraySize(VerboseData)); - addTests({&TgaImageConverterTest::r}); + addTests({&TgaImageConverterTest::uncompressedR}); + + addInstancedTests({&TgaImageConverterTest::rle}, + Containers::arraySize(RleData)); + + addTests({&TgaImageConverterTest::rleRgb, + &TgaImageConverterTest::rleRgba, + &TgaImageConverterTest::rleDisabled}); addInstancedTests({&TgaImageConverterTest::unsupportedMetadata}, Containers::arraySize(UnsupportedMetadataData)); @@ -133,7 +313,7 @@ constexpr char ConvertedDataRGB[] = { const ImageView2D OriginalRGB{PixelStorage{}.setSkip({0, 1, 0}), PixelFormat::RGB8Unorm, {2, 3}, OriginalDataRGB}; -void TgaImageConverterTest::rgb() { +void TgaImageConverterTest::uncompressedRgb() { auto&& data = VerboseData[testCaseInstanceId()]; setTestCaseDescription(data.name); @@ -143,6 +323,9 @@ void TgaImageConverterTest::rgb() { converter->setFlags(data.flags); + /* Disable RLE, that's tested in rle*() instead */ + converter->configuration().setValue("rle", false); + std::ostringstream out; Containers::Optional> array; { @@ -167,7 +350,7 @@ void TgaImageConverterTest::rgb() { CORRADE_COMPARE(out.str(), data.message24); } -/* Padding / skip tested in rgb() */ +/* Padding / skip tested in uncompressedRgb() */ constexpr char OriginalDataRGBA[] = { 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6, 4, 5, 6, 7, @@ -175,13 +358,16 @@ constexpr char OriginalDataRGBA[] = { }; const ImageView2D OriginalRGBA{PixelFormat::RGBA8Unorm, {2, 3}, OriginalDataRGBA}; -void TgaImageConverterTest::rgba() { +void TgaImageConverterTest::uncompressedRgba() { auto&& data = VerboseData[testCaseInstanceId()]; setTestCaseDescription(data.name); Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); converter->setFlags(data.flags); + /* Disable RLE, that's tested in rle*() instead */ + converter->configuration().setValue("rle", false); + std::ostringstream out; Containers::Optional> array; { @@ -206,7 +392,7 @@ void TgaImageConverterTest::rgba() { CORRADE_COMPARE(out.str(), data.message32); } -/* Padding / skip tested in rgb() */ +/* Padding / skip tested in uncompressedRgb() */ constexpr char OriginalDataR[] = { 1, 2, 3, 4, @@ -214,9 +400,12 @@ constexpr char OriginalDataR[] = { }; const ImageView2D OriginalR{PixelStorage{}.setAlignment(1), PixelFormat::R8Unorm, {2, 3}, OriginalDataR}; -void TgaImageConverterTest::r() { +void TgaImageConverterTest::uncompressedR() { Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); + /* Disable RLE, that's tested in rle*() instead */ + converter->configuration().setValue("rle", false); + Containers::Optional> array = converter->convertToData(OriginalR); CORRADE_VERIFY(array); @@ -235,6 +424,148 @@ void TgaImageConverterTest::r() { TestSuite::Compare::Container); } +void TgaImageConverterTest::rle() { + auto&& data = RleData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + CORRADE_COMPARE_AS(data.data.size(), data.width, + TestSuite::Compare::Divisible); + Vector2i size{data.width, Int(data.data.size()/data.width)}; + /* Skip/alignment handling tested in rleRgb() */ + ImageView2D image{PixelStorage{}.setAlignment(1), PixelFormat::R8Unorm, size, data.data}; + + Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); + + if(data.rleAcrossScanlines) + converter->configuration().setValue("rleAcrossScanlines", *data.rleAcrossScanlines); + + Containers::Optional> array = converter->convertToData(image); + CORRADE_VERIFY(array); + CORRADE_COMPARE_AS( + Containers::arrayCast(*array) + .exceptPrefix(sizeof(Implementation::TgaHeader)), + data.expected, + TestSuite::Compare::Container); + + if(!(_importerManager.loadState("TgaImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("TgaImporter plugin not enabled, can't test the result"); + + Containers::Pointer importer = _importerManager.instantiate("TgaImporter"); + CORRADE_VERIFY(importer->openData(*array)); + Containers::Optional converted = importer->image2D(0); + CORRADE_VERIFY(converted); + + CORRADE_COMPARE(converted->size(), size); + CORRADE_COMPARE(converted->format(), PixelFormat::R8Unorm); + CORRADE_COMPARE_AS(converted->data(), + data.data, + TestSuite::Compare::Container); +} + +void TgaImageConverterTest::rleRgb() { + Color3ub data[]{ + {}, {}, {}, {}, + /* Three different pixels, differing always in only one component */ + 0x0000ff_rgb, 0x0000ef_rgb, 0x0100ef_rgb, {}, + /* One different and two same pixels */ + 0x0100ef_rgb, 0xaabbcc_rgb, 0xaabbcc_rgb, {}, + }; + + ImageView2D image{PixelStorage{}.setRowLength(4).setSkip({0, 1, 0}), PixelFormat::RGB8Unorm, {3, 2}, data}; + + Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); + Containers::Optional> array = converter->convertToData(image); + CORRADE_VERIFY(array); + CORRADE_COMPARE_AS( + Containers::arrayCast(*array) + .exceptPrefix(sizeof(Implementation::TgaHeader)), + Containers::arrayView({ + /* Swizzled to BGR */ + 0x00|2, 0xff, 0x00, 0x00, + 0xef, 0x00, 0x00, + 0xef, 0x00, 0x01, + /* No runs across rows by default */ + 0x00|0, 0xef, 0x00, 0x01, + 0x80|1, 0xcc, 0xbb, 0xaa, + }), + TestSuite::Compare::Container); + + if(!(_importerManager.loadState("TgaImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("TgaImporter plugin not enabled, can't test the result"); + + Containers::Pointer importer = _importerManager.instantiate("TgaImporter"); + CORRADE_VERIFY(importer->openData(*array)); + Containers::Optional converted = importer->image2D(0); + CORRADE_VERIFY(converted); + + CORRADE_COMPARE(converted->size(), (Vector2i{3, 2})); + CORRADE_COMPARE(converted->format(), PixelFormat::RGB8Unorm); + CORRADE_COMPARE_AS(converted->data(), Containers::arrayCast(Containers::arrayView({ + 0x0000ff_rgb, 0x0000ef_rgb, 0x0100ef_rgb, + 0x0100ef_rgb, 0xaabbcc_rgb, 0xaabbcc_rgb, + })), TestSuite::Compare::Container); +} + +const Color4ub RleRgbaData[]{ + /* Four different pixels, differing always in only one component */ + 0x0000ffff_rgba, 0x0000efff_rgba, 0x0100efff_rgba, 0x0100effe_rgba, + /* One different and three same pixels */ + 0x0100effe_rgba, 0xaabbccdd_rgba, 0xaabbccdd_rgba, 0xaabbccdd_rgba +}; + +void TgaImageConverterTest::rleRgba() { + ImageView2D image{PixelFormat::RGBA8Unorm, {4, 2}, RleRgbaData}; + + Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); + Containers::Optional> array = converter->convertToData(image); + CORRADE_VERIFY(array); + CORRADE_COMPARE_AS( + Containers::arrayCast(*array) + .exceptPrefix(sizeof(Implementation::TgaHeader)), + Containers::arrayView({ + /* Swizzled to BGRA */ + 0x00|3, 0xff, 0x00, 0x00, 0xff, + 0xef, 0x00, 0x00, 0xff, + 0xef, 0x00, 0x01, 0xff, + 0xef, 0x00, 0x01, 0xfe, + /* No runs across rows by default */ + 0x00|0, 0xef, 0x00, 0x01, 0xfe, + 0x80|2, 0xcc, 0xbb, 0xaa, 0xdd + }), + TestSuite::Compare::Container); + + if(!(_importerManager.loadState("TgaImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("TgaImporter plugin not enabled, can't test the result"); + + Containers::Pointer importer = _importerManager.instantiate("TgaImporter"); + CORRADE_VERIFY(importer->openData(*array)); + Containers::Optional converted = importer->image2D(0); + CORRADE_VERIFY(converted); + + CORRADE_COMPARE(converted->size(), (Vector2i{4, 2})); + CORRADE_COMPARE(converted->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE_AS(converted->data(), + Containers::arrayCast(RleRgbaData), + TestSuite::Compare::Container); +} + +void TgaImageConverterTest::rleDisabled() { + ImageView2D image{PixelFormat::RGBA8Unorm, {4, 2}, RleRgbaData}; + + Containers::Pointer converter = _converterManager.instantiate("TgaImageConverter"); + converter->configuration().setValue("rle", false); + + const Containers::Optional> array = converter->convertToData(image); + CORRADE_VERIFY(array); + CORRADE_COMPARE_AS(array->exceptPrefix(sizeof(Implementation::TgaHeader)),Containers::arrayCast(Containers::arrayView({ + /* Swizzled to BGRA */ + 0xff0000ff_rgba, 0xef0000ff_rgba, 0xef0001ff_rgba, 0xef0001fe_rgba, + 0xef0001fe_rgba, 0xccbbaadd_rgba, 0xccbbaadd_rgba, 0xccbbaadd_rgba + })), TestSuite::Compare::Container); + + /* No need to verify a roundtrip, that's tested enough in uncompressed*() */ +} + void TgaImageConverterTest::unsupportedMetadata() { auto&& data = UnsupportedMetadataData[testCaseInstanceId()]; setTestCaseDescription(data.name); diff --git a/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.conf b/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.conf index e69de29bb..0bc7ae0f1 100644 --- a/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.conf +++ b/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.conf @@ -0,0 +1,10 @@ +[configuration] +# [configuration_] +# Run-length encode the data for smaller file size +rle=true + +# Allow RLE to go across scanlines. Can result in even smaller files but +# considered invalid in the TGA 2.0 specification and thus may cause issues +# in certain importers. +rleAcrossScanlines=false +# [configuration_] diff --git a/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.cpp b/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.cpp index 1662c55bc..f74282b02 100644 --- a/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.cpp +++ b/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.cpp @@ -28,9 +28,11 @@ #include #include #include +#include #include #include #include +#include #include #include "Magnum/ImageView.h" @@ -58,15 +60,173 @@ Containers::String TgaImageConverter::doMimeType() const { return "image/x-tga"_s; } +template T swizzle(const T& value); +template<> inline UnsignedByte swizzle(const UnsignedByte& value) { + return value; +} +template<> inline Vector3ub swizzle(const Vector3ub& value) { + return Math::gather<'b', 'g', 'r'>(value); +} +template<> inline Vector4ub swizzle(const Vector4ub& value) { + return Math::gather<'b', 'g', 'r', 'a'>(value); +} + +template void rleEncode(Containers::Array& data, const ImageView2D& image, const bool rleAcrossScanlines) { + /* Pixel array and current position in it. Can't iterate linearly in data() + because the input may have arbitrary padding between rows. */ + const Containers::StridedArrayView2D pixels = image.pixels(); + std::size_t y = 0; + std::size_t x = 1; + if(pixels.size()[1] == 1) { + y = 1; + x = 0; + } + + /* Value of previous pixel, as an union to make it easy to append to the + char array. Pre-swizzled so we don't need to swizzle in each + arrayAppend() call. */ + union { + T pixel; + char data[sizeof(T)]; + } prev{swizzle(pixels[0][0])}; + + /* Offset where a sequence run header placeholder is stored. Gets filled + with the actual count once the sequence ends. */ + Containers::Optional sequenceRunHeaderOffset; + /* Size of a sequence run / repeat count in a repeat run. If 1, it can be + either of the two, if > 1 then it depends on whether + sequenceRunHeaderOffset is NullOpt or not. */ + std::size_t count = 1; + + /* Go through all pixels, cache the row access for better perf on debug + builds */ + Containers::StridedArrayView1D currentRow; + while(y < pixels.size()[0]) { + if(!currentRow) currentRow = pixels[y]; + /* Current pixel, again pre-swizzled so we don't need to swizzle in + each arrayAppend() call */ + const T current = swizzle(currentRow[x]); + + /* Reset the counter if it's 128, as we can't store more than that, or + if we're at the new scanline and RLE across scanlines is disabled */ + if(count == 128 || (x == 0 && !rleAcrossScanlines)) { + if(sequenceRunHeaderOffset) { + arrayAppend(data, prev.data); + /* The amount of data written since the header be should equal + to the sequence run size */ + CORRADE_INTERNAL_ASSERT(data.size() - *sequenceRunHeaderOffset - 1 == count*sizeof(T)); + data[*sequenceRunHeaderOffset] = char(UnsignedByte(0x00|(count - 1))); + sequenceRunHeaderOffset = {}; + } else { + /* If it's just one pixel, make it a sequence instead for + consistency */ + arrayAppend(data, count == 1 ? '\x00' : char(UnsignedByte(0x80|(count - 1)))); + arrayAppend(data, prev.data); + } + + count = 0; + + /* Otherwise, if the next pixel is same like previous, count towards a + repeat run */ + } else if(current == prev.pixel) { + /* There was a sequence run before, finish it with the value before + the previous pixel (i.e., so both the previous and current pixel + are a part of the new repeat run) */ + if(sequenceRunHeaderOffset) { + /* If count is 1, runHeader should be nullptr */ + CORRADE_INTERNAL_ASSERT(count > 1); + /* The amount of data written since the header be should equal + to the sequence run size (excluding the previous pixel) */ + CORRADE_INTERNAL_ASSERT(data.size() - *sequenceRunHeaderOffset - 1 == (count - 1)*sizeof(T)); + data[*sequenceRunHeaderOffset] = char(UnsignedByte(0x00|(count - 2))); + sequenceRunHeaderOffset = {}; + count = 1; + } + + /* Otherwise, if the current pixel is different from the previous, + count towards a sequence run */ + } else { + /* If we don't have a sequence run header written yet, it can mean + that there's either a repeat run, or the previous pixel was + also different */ + if(!sequenceRunHeaderOffset) { + /* If the previous pixel was standalone, write it with a + placeholder for a sequence run header before. If the next + pixel is different from the current one, this run will be + extended, otherwise it'll be ended and a new repeat run will + be started from the current pixel. */ + if(count == 1) { + sequenceRunHeaderOffset = data.size(); + arrayAppend(data, NoInit, 1); + arrayAppend(data, prev.data); + /* Keeping count at 1 */ + + /* Otherwise, there was a repeat run before. Finish it with the + previous pixel (i.e., so the current pixel is a start of a + new run). */ + } else { + arrayAppend(data, char(UnsignedByte(0x80|(count - 1)))); + arrayAppend(data, prev.data); + count = 0; + } + + /* If we have a sequence run header written, write the prev pixel. + *Not* the current one because it might be the beginning of a + repeat run. */ + } else arrayAppend(data, prev.data); + } + + prev.pixel = current; + ++count; + ++x; + + if(x == pixels.size()[1]) { + ++y; + currentRow = nullptr; + x = 0; + } + } + + /* We should be at the end of the input and there should be at least one + yet-unwritten pixel left */ + CORRADE_INTERNAL_ASSERT(x == 0 && y == pixels.size()[0]); + CORRADE_INTERNAL_ASSERT(count >= 1); + + /* If there's an unfinished sequence run header, write the count to it, + and put the last unwritten pixel there as well */ + if(sequenceRunHeaderOffset) { + arrayAppend(data, prev.data); + /* The amount of data written since the header should be again equal to + the sequence run size */ + CORRADE_INTERNAL_ASSERT(data.size() - *sequenceRunHeaderOffset - 1 == count*sizeof(T)); + data[*sequenceRunHeaderOffset] = char(UnsignedByte(0x00|(count - 1))); + + /* Otherwise write a repeat header with the last pixel */ + } else { + /* If it's just one pixel, make it a sequence instead for consistency */ + arrayAppend(data, count == 1 ? '\x00' : char(UnsignedByte(0x80|(count - 1)))); + arrayAppend(data, prev.data); + } +} + Containers::Optional> TgaImageConverter::doConvertToData(const ImageView2D& image) { /* Warn about lost metadata */ if(image.flags() & ImageFlag2D::Array) { Warning{} << "Trade::TgaImageConverter::convertToData(): 1D array images are unrepresentable in TGA, saving as a regular 2D image"; } - /* Initialize data buffer */ + /* Initialize data buffer. If we're writing a RLE-encoded file, create a + growable array (which we have to copy to a non-growable after after), if + not then allocate exactly the amount of bytes so we don't need to copy + after. */ const auto pixelSize = UnsignedByte(image.pixelSize()); - Containers::Array data{NoInit, sizeof(Implementation::TgaHeader) + pixelSize*image.size().product()}; + const bool rle = configuration().value("rle"); + Containers::Array data; + if(rle) { + arrayAppend(data, NoInit, sizeof(Implementation::TgaHeader)); + } else { + data = Containers::Array{NoInit, sizeof(Implementation::TgaHeader) + pixelSize*image.size().product()}; + } /* Clear the header and fill non-zero values */ auto& header = *reinterpret_cast(data.begin()); @@ -93,17 +253,42 @@ Containers::Optional> TgaImageConverter::doConvertToData header.width = UnsignedShort(Utility::Endianness::littleEndian(image.size().x())); header.height = UnsignedShort(Utility::Endianness::littleEndian(image.size().y())); - /* Copy the pixels into output, dropping padding (if any) */ - const Containers::ArrayView pixels = data.exceptPrefix(sizeof(Implementation::TgaHeader)); - Utility::copy(image.pixels(), Containers::StridedArrayView3D{pixels, - {std::size_t(image.size().y()), std::size_t(image.size().x()), pixelSize}}); - - if(image.format() == PixelFormat::RGB8Unorm) { - for(Vector3ub& pixel: Containers::arrayCast(pixels)) - pixel = Math::gather<'b', 'g', 'r'>(pixel); - } else if(image.format() == PixelFormat::RGBA8Unorm) { - for(Vector4ub& pixel: Containers::arrayCast(pixels)) - pixel = Math::gather<'b', 'g', 'r', 'a'>(pixel); + /* Perform RLE encoding */ + if(rle) { + header.imageType |= 8; + + const bool rleAcrossScanlines = configuration().value("rleAcrossScanlines"); + switch(image.format()) { + case PixelFormat::R8Unorm: + rleEncode(data, image, rleAcrossScanlines); + break; + case PixelFormat::RGB8Unorm: + rleEncode(data, image, rleAcrossScanlines); + break; + case PixelFormat::RGBA8Unorm: + rleEncode(data, image, rleAcrossScanlines); + break; + default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + /* Turn the array back into a non-growable to avoid a dangling deleter + on plugin unload */ + arrayShrink(data); + + /* Otherwise directly dopy the pixels into output, dropping padding (if + any), and do the BGR(A) conversion on the copied data */ + } else { + const Containers::ArrayView pixels = data.exceptPrefix(sizeof(Implementation::TgaHeader)); + Utility::copy(image.pixels(), Containers::StridedArrayView3D{pixels, + {std::size_t(image.size().y()), std::size_t(image.size().x()), pixelSize}}); + + if(image.format() == PixelFormat::RGB8Unorm) { + for(Vector3ub& pixel: Containers::arrayCast(pixels)) + pixel = Math::gather<'b', 'g', 'r'>(pixel); + } else if(image.format() == PixelFormat::RGBA8Unorm) { + for(Vector4ub& pixel: Containers::arrayCast(pixels)) + pixel = Math::gather<'b', 'g', 'r', 'a'>(pixel); + } } /* GCC 4.8 needs extra help here */ diff --git a/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.h b/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.h index 1eccd9b35..a7ce08c8c 100644 --- a/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.h +++ b/src/MagnumPlugins/TgaImageConverter/TgaImageConverter.h @@ -91,8 +91,11 @@ information. @section Trade-TgaImageConverter-behavior Behavior and limitations -The output is always uncompressed. If you want to make use of RLE compression -and have the files smaller, use the @ref StbImageConverter plugin instead. +The output is RLE-compressed by default, you can produce uncompressed files by +disabling the @cb{.ini} rle @ce @ref Trade-TgaImageConverter-configuration "configuration option". +Enabling @cb{.ini} rleAcrossScanlines @ce will result in even smaller files +but [such files are considered invalid in the TGA 2.0 spec](https://en.wikipedia.org/wiki/Truevision_TGA#Specification_discrepancies) +and thus may cause issues in certain importers. The TGA file format doesn't have a way to distinguish between 2D and 1D array images. If an image has @ref ImageFlag2D::Array set, a warning is printed and @@ -101,6 +104,16 @@ the file is saved as a regular 2D image. While TGA files can have several extensions, @ref extension() always returns @cpp "tga" @ce as that's the most common one. As TGA doesn't have a registered MIME type, @ref mimeType() returns @cpp "image/x-tga" @ce. + +@section Trade-TgaImageConverter-configuration Plugin-specific configuration + +It's possible to tune various output options through @ref configuration(). See +below for all options and their default values: + +@snippet MagnumPlugins/TgaImageConverter/TgaImageConverter.conf configuration_ + +See @ref plugins-configuration for more information and an example showing how +to edit the configuration values. */ class MAGNUM_TGAIMAGECONVERTER_EXPORT TgaImageConverter: public AbstractImageConverter { public: