From 5796b597f3bac42b0e0df82a9c1421cf671c7920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 15 Jan 2023 20:45:55 +0100 Subject: [PATCH] TgaImageConverter: implement RLE encoding. Because I got tired of having to manually postprocess ground truth files for shader tests. A bit strange / funny that the first ever version of my code manages to produce smaller files than both stb_image and ImageMagick (with -compress RunTimeLengthEncoded), for some reason both pick some strange inefficient run combinations in various scenarios, such as "copy 2" + "repeat 3" where I pick "repeat 5". And the difference is not insignificant -- when testing with some shader test files, it resulted in a ~17% smaller size! The plugin follows the stricter variant of the spec by default (i.e., splitting runs across scanline boundaries) -- so that's *not* the reason for the weird differences between my code and theirs -- but provides an option to not do that for even smaller files. Which I'm going to use for shader ground truth test files, because there every byte counts. This option together with the above difference causes files to be ~25% smaller, which is quite a lot. --- doc/changelog.dox | 2 + .../Test/TgaImageConverterTest.cpp | 353 +++++++++++++++++- .../TgaImageConverter/TgaImageConverter.conf | 10 + .../TgaImageConverter/TgaImageConverter.cpp | 211 ++++++++++- .../TgaImageConverter/TgaImageConverter.h | 17 +- 5 files changed, 567 insertions(+), 26 deletions(-) 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: