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