From 440a3f96adcf1d8d003c13e64fce1c59ca2a11ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 19 Mar 2020 10:37:59 +0100 Subject: [PATCH] TgaImporter: implement RLE decoding. I need a bunch of new test images for shader tests and this was rather easy to do, considering RLE images are ten times smaller. Not doing the converter side yet due to time constraints, temporarily relying on imagemagick to handle that (StbImageConverter does RLE also, but interestingly it produces larger files than imagemagick). --- doc/changelog.dox | 1 + .../TgaImporter/Test/TgaImporterTest.cpp | 243 +++++++++++++----- src/MagnumPlugins/TgaImporter/TgaImporter.cpp | 75 +++++- src/MagnumPlugins/TgaImporter/TgaImporter.h | 5 +- 4 files changed, 254 insertions(+), 70 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 10cecb1ce..46def72a0 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -236,6 +236,7 @@ See also: - @ref Trade::PhongMaterialData now supports both color and texture instead of just one or the other, can reference normal textures as well and specfify texture coordinate transform +- RLE compression support in @ref Trade::TgaImporter "TgaImporter" @subsubsection changelog-latest-new-vk Vk library diff --git a/src/MagnumPlugins/TgaImporter/Test/TgaImporterTest.cpp b/src/MagnumPlugins/TgaImporter/Test/TgaImporterTest.cpp index d3ed2fe32..6f323b41e 100644 --- a/src/MagnumPlugins/TgaImporter/Test/TgaImporterTest.cpp +++ b/src/MagnumPlugins/TgaImporter/Test/TgaImporterTest.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "Magnum/PixelFormat.h" #include "Magnum/Trade/AbstractImporter.h" @@ -43,18 +44,20 @@ struct TgaImporterTest: TestSuite::Tester { explicit TgaImporterTest(); void openEmpty(); - void openShortHeader(); - void openShortData(); + void openShort(); void paletted(); - void compressed(); + void invalid(); + void unsupportedBits(); - void colorBits16(); - void colorBits24(); - void colorBits32(); + void color24(); + void color24Rle(); + void color32(); + void color32Rle(); + void grayscale8(); + void grayscale8Rle(); - void grayscaleBits8(); - void grayscaleBits16(); + void rleTooLarge(); void openTwice(); void importTwice(); @@ -63,22 +66,73 @@ struct TgaImporterTest: TestSuite::Tester { PluginManager::Manager _manager{"nonexistent"}; }; +constexpr struct { + const char* name; + char imageType; + char bpp; + const char* message; +} UnsupportedBitsData[] { + {"color 16", 2, 16, "unsupported color bits-per-pixel: 16"}, + {"grayscale 16", 3, 16, "unsupported grayscale bits-per-pixel: 16"}, + {"RLE color 16", 10, 16, "unsupported color bits-per-pixel: 16"}, + {"RLE grayscale 16", 11, 16, "unsupported grayscale bits-per-pixel: 16"} +}; + +constexpr const char Color24[] = { + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 24, 0, + 1, 2, 3, 2, 3, 4, + 3, 4, 5, 4, 5, 6, + 5, 6, 7, 6, 7, 8 +}; + +constexpr const char Color24Rle[] = { + 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 24, 0, + /* 3 pixels as-is */ + '\x02', 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + /* 1 pixel 3x repeated */ + '\x82', 4, 5, 6 +}; + +constexpr struct { + const char* name; + Containers::ArrayView data; + const char* message; +} ShortData[] { + {"short header", Containers::arrayView(Color24).prefix(17), + "the file is too short: 17 bytes"}, + {"short data", Containers::arrayView(Color24).except(1), + "the file is too short: got 35 bytes but expected 36"}, + {"short RLE data", Containers::arrayView(Color24Rle).except(1), + "RLE file too short at pixel 3"}, + {"short RLE raw data", Containers::arrayView(Color24Rle).except(5), + "RLE file too short at pixel 0"} +}; + TgaImporterTest::TgaImporterTest() { - addTests({&TgaImporterTest::openEmpty, - &TgaImporterTest::openShortHeader, - &TgaImporterTest::openShortData, + addTests({&TgaImporterTest::openEmpty}); - &TgaImporterTest::paletted, - &TgaImporterTest::compressed, + addInstancedTests({&TgaImporterTest::openShort}, + Containers::arraySize(ShortData)); - &TgaImporterTest::colorBits16, - &TgaImporterTest::colorBits24, - &TgaImporterTest::colorBits32, + addTests({&TgaImporterTest::paletted, + &TgaImporterTest::invalid}); - &TgaImporterTest::grayscaleBits8, - &TgaImporterTest::grayscaleBits16, + addInstancedTests({ + &TgaImporterTest::unsupportedBits}, + Containers::arraySize(UnsupportedBitsData)); - &TgaImporterTest::openTwice, + addTests({&TgaImporterTest::color24, + &TgaImporterTest::color24Rle, + &TgaImporterTest::color32, + &TgaImporterTest::color32Rle, + &TgaImporterTest::grayscale8, + &TgaImporterTest::grayscale8Rle, + + &TgaImporterTest::rleTooLarge}); + + addTests({&TgaImporterTest::openTwice, &TgaImporterTest::importTwice}); /* Load the plugin directly from the build tree. Otherwise it's static and @@ -99,33 +153,18 @@ void TgaImporterTest::openEmpty() { CORRADE_COMPARE(out.str(), "Trade::TgaImporter::openData(): the file is empty\n"); } -void TgaImporterTest::openShortHeader() { - Containers::Pointer importer = _manager.instantiate("TgaImporter"); - const char data[] = { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - CORRADE_VERIFY(importer->openData(data)); - - std::ostringstream debug; - Error redirectError{&debug}; - CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): the file is too short: 17 bytes\n"); -} +void TgaImporterTest::openShort() { + auto&& data = ShortData[testCaseInstanceId()]; + setTestCaseDescription(data.name); -constexpr const char ColorBits24[] = { - 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 24, 0, - 1, 2, 3, 2, 3, 4, - 3, 4, 5, 4, 5, 6, - 5, 6, 7, 6, 7, 8 -}; - -void TgaImporterTest::openShortData() { Containers::Pointer importer = _manager.instantiate("TgaImporter"); - CORRADE_VERIFY(importer->openData(Containers::arrayView(ColorBits24).except(1))); + CORRADE_VERIFY(importer->openData(data.data)); - std::ostringstream debug; - Error redirectError{&debug}; + std::ostringstream out; + Error redirectError{&out}; CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): the file is too short: got 35 bytes but expected 36\n"); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::TgaImporter::image2D(): {}\n", data.message)); } void TgaImporterTest::paletted() { @@ -139,7 +178,7 @@ void TgaImporterTest::paletted() { CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): paletted files are not supported\n"); } -void TgaImporterTest::compressed() { +void TgaImporterTest::invalid() { Containers::Pointer importer = _manager.instantiate("TgaImporter"); const char data[] = { 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; CORRADE_VERIFY(importer->openData(data)); @@ -147,28 +186,51 @@ void TgaImporterTest::compressed() { std::ostringstream debug; Error redirectError{&debug}; CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): unsupported (compressed?) image type: 9\n"); + CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): unsupported image type: 9\n"); } -void TgaImporterTest::colorBits16() { +void TgaImporterTest::unsupportedBits() { + auto&& data = UnsupportedBitsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + Containers::Pointer importer = _manager.instantiate("TgaImporter"); - const char data[] = { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0 }; - CORRADE_VERIFY(importer->openData(data)); + const char imageData[] = { + 0, 0, data.imageType, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, data.bpp, 0 + }; + CORRADE_VERIFY(importer->openData(imageData)); - std::ostringstream debug; - Error redirectError{&debug}; + std::ostringstream out; + Error redirectError{&out}; CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): unsupported color bits-per-pixel: 16\n"); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::TgaImporter::image2D(): {}\n", data.message)); } -void TgaImporterTest::colorBits24() { +void TgaImporterTest::color24() { Containers::Pointer importer = _manager.instantiate("TgaImporter"); const char pixels[] = { 3, 2, 1, 4, 3, 2, 5, 4, 3, 6, 5, 4, 7, 6, 5, 8, 7, 6 }; - CORRADE_VERIFY(importer->openData(ColorBits24)); + CORRADE_VERIFY(importer->openData(Color24)); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->storage().alignment(), 1); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Unorm); + CORRADE_COMPARE(image->size(), Vector2i(2, 3)); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(pixels), + TestSuite::Compare::Container); +} + +void TgaImporterTest::color24Rle() { + Containers::Pointer importer = _manager.instantiate("TgaImporter"); + const char pixels[] = { + 3, 2, 1, 4, 3, 2, + 5, 4, 3, 6, 5, 4, + 6, 5, 4, 6, 5, 4 + }; + CORRADE_VERIFY(importer->openData(Color24Rle)); Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); @@ -179,7 +241,7 @@ void TgaImporterTest::colorBits24() { TestSuite::Compare::Container); } -void TgaImporterTest::colorBits32() { +void TgaImporterTest::color32() { Containers::Pointer importer = _manager.instantiate("TgaImporter"); const char data[] = { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 32, 0, @@ -203,7 +265,35 @@ void TgaImporterTest::colorBits32() { TestSuite::Compare::Container); } -void TgaImporterTest::grayscaleBits8() { +void TgaImporterTest::color32Rle() { + Containers::Pointer importer = _manager.instantiate("TgaImporter"); + const char data[] = { + 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 32, 0, + /* 2 pixels repeated */ + '\x81', 1, 2, 3, 4, + /* 4 pixels as-is */ + '\x03', 3, 4, 5, 6, + 4, 5, 6, 7, + 5, 6, 7, 8, + 6, 7, 8, 9 + }; + const char pixels[] = { + 3, 2, 1, 4, 3, 2, 1, 4, + 5, 4, 3, 6, 6, 5, 4, 7, + 7, 6, 5, 8, 8, 7, 6, 9 + }; + CORRADE_VERIFY(importer->openData(data)); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->storage().alignment(), 4); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE(image->size(), Vector2i(2, 3)); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(pixels), + TestSuite::Compare::Container); +} + +void TgaImporterTest::grayscale8() { Containers::Pointer importer = _manager.instantiate("TgaImporter"); const char data[] = { 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 8, 0, @@ -222,15 +312,52 @@ void TgaImporterTest::grayscaleBits8() { TestSuite::Compare::Container); } -void TgaImporterTest::grayscaleBits16() { +void TgaImporterTest::grayscale8Rle() { Containers::Pointer importer = _manager.instantiate("TgaImporter"); - const char data[] = { 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0 }; + const char data[] = { + 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 8, 0, + /* 2 pixels as-is */ + '\x01', 1, 2, + /* 1 pixel 2x repeated */ + '\x81', 3, + /* 1 pixel as-is */ + '\x00', 5, + /* 1 pixel 1x repeated */ + '\x00', 6 + }; + const char pixels[] { + 1, 2, + 3, 3, + 5, 6 + }; CORRADE_VERIFY(importer->openData(data)); - std::ostringstream debug; - Error redirectError{&debug}; + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->storage().alignment(), 1); + CORRADE_COMPARE(image->format(), PixelFormat::R8Unorm); + CORRADE_COMPARE(image->size(), Vector2i(2, 3)); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView(pixels), + TestSuite::Compare::Container); +} + +void TgaImporterTest::rleTooLarge() { + Containers::Pointer importer = _manager.instantiate("TgaImporter"); + const char data[] = { + 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 24, 0, + /* 3 pixels as-is */ + '\x02', 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + /* 1 pixel 4x repeated (one more than it should be) */ + '\x83', 4, 5, 6 + }; + CORRADE_VERIFY(importer->openData(data)); + + std::ostringstream out; + Error redirectError{&out}; CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): unsupported grayscale bits-per-pixel: 16\n"); + CORRADE_COMPARE(out.str(), "Trade::TgaImporter::image2D(): RLE data larger than advertised Vector(2, 3) pixels at byte 28\n"); } void TgaImporterTest::openTwice() { diff --git a/src/MagnumPlugins/TgaImporter/TgaImporter.cpp b/src/MagnumPlugins/TgaImporter/TgaImporter.cpp index 723c4ad0d..211135707 100644 --- a/src/MagnumPlugins/TgaImporter/TgaImporter.cpp +++ b/src/MagnumPlugins/TgaImporter/TgaImporter.cpp @@ -92,7 +92,10 @@ Containers::Optional TgaImporter::doImage2D(UnsignedInt, UnsignedIn } /* Color */ - if(header.imageType == 2) { + bool rle = false; + if(header.imageType == 2 || header.imageType == 10) { + /* Reference: http://www.paulbourke.net/dataformats/tga/ */ + rle = header.imageType == 10; switch(header.bpp) { case 24: format = PixelFormat::RGB8Unorm; @@ -106,27 +109,79 @@ Containers::Optional TgaImporter::doImage2D(UnsignedInt, UnsignedIn } /* Grayscale */ - } else if(header.imageType == 3) { + } else if(header.imageType == 3 || header.imageType == 11) { + /* I only discovered this by accident when using ImageMagick's + mogrify -compression RunLengthEncoded file.tga + as far as I could find, it's not documented in any TGA specs */ + rle = header.imageType == 11; format = PixelFormat::R8Unorm; if(header.bpp != 8) { Error() << "Trade::TgaImporter::image2D(): unsupported grayscale bits-per-pixel:" << header.bpp; return Containers::NullOpt; } - /* Compressed files */ + /* Other? */ } else { - Error() << "Trade::TgaImporter::image2D(): unsupported (compressed?) image type:" << header.imageType; + Error() << "Trade::TgaImporter::image2D(): unsupported image type:" << header.imageType; return Containers::NullOpt; } - std::size_t outputSize = std::size_t(size.product())*header.bpp/8; - if(outputSize + sizeof(Implementation::TgaHeader) > _in.size()) { - Error{} << "Trade::TgaImporter::image2D(): the file is too short: got" << _in.size() << "bytes but expected" << outputSize + sizeof(Implementation::TgaHeader); - return Containers::NullOpt; - } + const std::size_t pixelSize = header.bpp/8; + const std::size_t outputSize = std::size_t(size.product())*pixelSize; + /* Copy data directly if not RLE */ Containers::Array data{outputSize}; - Utility::copy(_in.suffix(sizeof(Implementation::TgaHeader)), data); + Containers::ArrayView srcPixels = _in.suffix(sizeof(Implementation::TgaHeader)); + if(!rle) { + /* Files that are larger are allowed in this case (but not for RLE) */ + if(srcPixels.size() < outputSize) { + Error{} << "Trade::TgaImporter::image2D(): the file is too short: got" << _in.size() << "bytes but expected" << outputSize + sizeof(Implementation::TgaHeader); + return Containers::NullOpt; + } + + Utility::copy(srcPixels.prefix(data.size()), data); + + /* Otherwise decode */ + } else { + Containers::ArrayView dstPixels = data; + while(!srcPixels.empty()) { + /* Reference: http://www.paulbourke.net/dataformats/tga/ */ + + /* 8-bit RLE header. First bit denotes the operation, last 7 bits + denotes operation count minus 1. */ + const UnsignedByte rleHeader = srcPixels[0]; + const std::size_t count = (rleHeader & ~0x80) + 1; + + /* First bit set to 1 means copying the following pixel given + number of times, 0 means copying the following number of + pixels once. We represent that operation with a stride. */ + const std::size_t dataSize = (rleHeader & 0x80 ? 1 : count)*pixelSize; + const std::ptrdiff_t stride = rleHeader & 0x80 ? 0 : pixelSize; + + /* Check bounds */ + if(1 + dataSize > srcPixels.size()) { + Error{} << "Trade::TgaImporter::image2D(): RLE file too short at pixel" << (dstPixels.begin() - data.begin())/pixelSize; + return Containers::NullOpt; + } + if(count*pixelSize > dstPixels.size()) { + Error{} << "Trade::TgaImporter::image2D(): RLE data larger than advertised" << size << "pixels at byte" << (srcPixels.data() - _in.data()); + return Containers::NullOpt; + } + + /* Copy the data */ + Containers::StridedArrayView2D src{ + srcPixels.slice(1, 1 + dataSize), + {count, pixelSize}, {stride, 1}}; + Containers::StridedArrayView2D dst{ + dstPixels.prefix(count*pixelSize), + {count, pixelSize}}; + Utility::copy(src, dst); + + /* Update views for the next round */ + srcPixels = srcPixels.suffix(1 + dataSize); + dstPixels = dstPixels.suffix(count*pixelSize); + } + } /* Adjust pixel storage if row size is not four byte aligned */ PixelStorage storage; diff --git a/src/MagnumPlugins/TgaImporter/TgaImporter.h b/src/MagnumPlugins/TgaImporter/TgaImporter.h index 0b2085e17..386c9a80b 100644 --- a/src/MagnumPlugins/TgaImporter/TgaImporter.h +++ b/src/MagnumPlugins/TgaImporter/TgaImporter.h @@ -57,8 +57,9 @@ namespace Magnum { namespace Trade { /** @brief TGA importer plugin -Supports Truevision TGA (`*.tga`, `*.vda`, `*.icb`, `*.vst`) uncompressed BGR, -BGRA or grayscale images with 8 bits per channel. +Supports Truevision TGA (`*.tga`, `*.vda`, `*.icb`, `*.vst`) BGR, BGRA or +grayscale images with 8 bits per channel. RLE compression is supported, +paletted images are not. The images are imported with @ref PixelFormat::RGB8Unorm, @ref PixelFormat::RGBA8Unorm or @ref PixelFormat::R8Unorm, respectively. Images