Browse Source

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).
pull/430/head
Vladimír Vondruš 6 years ago
parent
commit
440a3f96ad
  1. 1
      doc/changelog.dox
  2. 243
      src/MagnumPlugins/TgaImporter/Test/TgaImporterTest.cpp
  3. 75
      src/MagnumPlugins/TgaImporter/TgaImporter.cpp
  4. 5
      src/MagnumPlugins/TgaImporter/TgaImporter.h

1
doc/changelog.dox

@ -236,6 +236,7 @@ See also:
- @ref Trade::PhongMaterialData now supports both color and texture instead - @ref Trade::PhongMaterialData now supports both color and texture instead
of just one or the other, can reference normal textures as well and of just one or the other, can reference normal textures as well and
specfify texture coordinate transform specfify texture coordinate transform
- RLE compression support in @ref Trade::TgaImporter "TgaImporter"
@subsubsection changelog-latest-new-vk Vk library @subsubsection changelog-latest-new-vk Vk library

243
src/MagnumPlugins/TgaImporter/Test/TgaImporterTest.cpp

@ -30,6 +30,7 @@
#include <Corrade/TestSuite/Compare/Container.h> #include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/Utility/DebugStl.h> #include <Corrade/Utility/DebugStl.h>
#include <Corrade/Utility/Directory.h> #include <Corrade/Utility/Directory.h>
#include <Corrade/Utility/FormatStl.h>
#include "Magnum/PixelFormat.h" #include "Magnum/PixelFormat.h"
#include "Magnum/Trade/AbstractImporter.h" #include "Magnum/Trade/AbstractImporter.h"
@ -43,18 +44,20 @@ struct TgaImporterTest: TestSuite::Tester {
explicit TgaImporterTest(); explicit TgaImporterTest();
void openEmpty(); void openEmpty();
void openShortHeader(); void openShort();
void openShortData();
void paletted(); void paletted();
void compressed(); void invalid();
void unsupportedBits();
void colorBits16(); void color24();
void colorBits24(); void color24Rle();
void colorBits32(); void color32();
void color32Rle();
void grayscale8();
void grayscale8Rle();
void grayscaleBits8(); void rleTooLarge();
void grayscaleBits16();
void openTwice(); void openTwice();
void importTwice(); void importTwice();
@ -63,22 +66,73 @@ struct TgaImporterTest: TestSuite::Tester {
PluginManager::Manager<AbstractImporter> _manager{"nonexistent"}; PluginManager::Manager<AbstractImporter> _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<const char> 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() { TgaImporterTest::TgaImporterTest() {
addTests({&TgaImporterTest::openEmpty, addTests({&TgaImporterTest::openEmpty});
&TgaImporterTest::openShortHeader,
&TgaImporterTest::openShortData,
&TgaImporterTest::paletted, addInstancedTests({&TgaImporterTest::openShort},
&TgaImporterTest::compressed, Containers::arraySize(ShortData));
&TgaImporterTest::colorBits16, addTests({&TgaImporterTest::paletted,
&TgaImporterTest::colorBits24, &TgaImporterTest::invalid});
&TgaImporterTest::colorBits32,
&TgaImporterTest::grayscaleBits8, addInstancedTests({
&TgaImporterTest::grayscaleBits16, &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}); &TgaImporterTest::importTwice});
/* Load the plugin directly from the build tree. Otherwise it's static and /* 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"); CORRADE_COMPARE(out.str(), "Trade::TgaImporter::openData(): the file is empty\n");
} }
void TgaImporterTest::openShortHeader() { void TgaImporterTest::openShort() {
Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter"); auto&& data = ShortData[testCaseInstanceId()];
const char data[] = { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; setTestCaseDescription(data.name);
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");
}
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<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter");
CORRADE_VERIFY(importer->openData(Containers::arrayView(ColorBits24).except(1))); CORRADE_VERIFY(importer->openData(data.data));
std::ostringstream debug; std::ostringstream out;
Error redirectError{&debug}; Error redirectError{&out};
CORRADE_VERIFY(!importer->image2D(0)); 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() { void TgaImporterTest::paletted() {
@ -139,7 +178,7 @@ void TgaImporterTest::paletted() {
CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): paletted files are not supported\n"); CORRADE_COMPARE(debug.str(), "Trade::TgaImporter::image2D(): paletted files are not supported\n");
} }
void TgaImporterTest::compressed() { void TgaImporterTest::invalid() {
Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> 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 }; 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)); CORRADE_VERIFY(importer->openData(data));
@ -147,28 +186,51 @@ void TgaImporterTest::compressed() {
std::ostringstream debug; std::ostringstream debug;
Error redirectError{&debug}; Error redirectError{&debug};
CORRADE_VERIFY(!importer->image2D(0)); 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<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> 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 }; const char imageData[] = {
CORRADE_VERIFY(importer->openData(data)); 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; std::ostringstream out;
Error redirectError{&debug}; Error redirectError{&out};
CORRADE_VERIFY(!importer->image2D(0)); 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<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter");
const char pixels[] = { const char pixels[] = {
3, 2, 1, 4, 3, 2, 3, 2, 1, 4, 3, 2,
5, 4, 3, 6, 5, 4, 5, 4, 3, 6, 5, 4,
7, 6, 5, 8, 7, 6 7, 6, 5, 8, 7, 6
}; };
CORRADE_VERIFY(importer->openData(ColorBits24)); CORRADE_VERIFY(importer->openData(Color24));
Containers::Optional<Trade::ImageData2D> 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<AbstractImporter> 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<Trade::ImageData2D> image = importer->image2D(0); Containers::Optional<Trade::ImageData2D> image = importer->image2D(0);
CORRADE_VERIFY(image); CORRADE_VERIFY(image);
@ -179,7 +241,7 @@ void TgaImporterTest::colorBits24() {
TestSuite::Compare::Container); TestSuite::Compare::Container);
} }
void TgaImporterTest::colorBits32() { void TgaImporterTest::color32() {
Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter");
const char data[] = { const char data[] = {
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 32, 0, 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); TestSuite::Compare::Container);
} }
void TgaImporterTest::grayscaleBits8() { void TgaImporterTest::color32Rle() {
Containers::Pointer<AbstractImporter> 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<Trade::ImageData2D> 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<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter");
const char data[] = { const char data[] = {
0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 8, 0, 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); TestSuite::Compare::Container);
} }
void TgaImporterTest::grayscaleBits16() { void TgaImporterTest::grayscale8Rle() {
Containers::Pointer<AbstractImporter> importer = _manager.instantiate("TgaImporter"); Containers::Pointer<AbstractImporter> 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)); CORRADE_VERIFY(importer->openData(data));
std::ostringstream debug; Containers::Optional<Trade::ImageData2D> image = importer->image2D(0);
Error redirectError{&debug}; 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<AbstractImporter> 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_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() { void TgaImporterTest::openTwice() {

75
src/MagnumPlugins/TgaImporter/TgaImporter.cpp

@ -92,7 +92,10 @@ Containers::Optional<ImageData2D> TgaImporter::doImage2D(UnsignedInt, UnsignedIn
} }
/* Color */ /* 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) { switch(header.bpp) {
case 24: case 24:
format = PixelFormat::RGB8Unorm; format = PixelFormat::RGB8Unorm;
@ -106,27 +109,79 @@ Containers::Optional<ImageData2D> TgaImporter::doImage2D(UnsignedInt, UnsignedIn
} }
/* Grayscale */ /* 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; format = PixelFormat::R8Unorm;
if(header.bpp != 8) { if(header.bpp != 8) {
Error() << "Trade::TgaImporter::image2D(): unsupported grayscale bits-per-pixel:" << header.bpp; Error() << "Trade::TgaImporter::image2D(): unsupported grayscale bits-per-pixel:" << header.bpp;
return Containers::NullOpt; return Containers::NullOpt;
} }
/* Compressed files */ /* Other? */
} else { } else {
Error() << "Trade::TgaImporter::image2D(): unsupported (compressed?) image type:" << header.imageType; Error() << "Trade::TgaImporter::image2D(): unsupported image type:" << header.imageType;
return Containers::NullOpt; return Containers::NullOpt;
} }
std::size_t outputSize = std::size_t(size.product())*header.bpp/8; const std::size_t pixelSize = header.bpp/8;
if(outputSize + sizeof(Implementation::TgaHeader) > _in.size()) { const std::size_t outputSize = std::size_t(size.product())*pixelSize;
Error{} << "Trade::TgaImporter::image2D(): the file is too short: got" << _in.size() << "bytes but expected" << outputSize + sizeof(Implementation::TgaHeader);
return Containers::NullOpt;
}
/* Copy data directly if not RLE */
Containers::Array<char> data{outputSize}; Containers::Array<char> data{outputSize};
Utility::copy(_in.suffix(sizeof(Implementation::TgaHeader)), data); Containers::ArrayView<const char> 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<char> 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<const char> src{
srcPixels.slice(1, 1 + dataSize),
{count, pixelSize}, {stride, 1}};
Containers::StridedArrayView2D<char> 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 */ /* Adjust pixel storage if row size is not four byte aligned */
PixelStorage storage; PixelStorage storage;

5
src/MagnumPlugins/TgaImporter/TgaImporter.h

@ -57,8 +57,9 @@ namespace Magnum { namespace Trade {
/** /**
@brief TGA importer plugin @brief TGA importer plugin
Supports Truevision TGA (`*.tga`, `*.vda`, `*.icb`, `*.vst`) uncompressed BGR, Supports Truevision TGA (`*.tga`, `*.vda`, `*.icb`, `*.vst`) BGR, BGRA or
BGRA or grayscale images with 8 bits per channel. grayscale images with 8 bits per channel. RLE compression is supported,
paletted images are not.
The images are imported with @ref PixelFormat::RGB8Unorm, The images are imported with @ref PixelFormat::RGB8Unorm,
@ref PixelFormat::RGBA8Unorm or @ref PixelFormat::R8Unorm, respectively. Images @ref PixelFormat::RGBA8Unorm or @ref PixelFormat::R8Unorm, respectively. Images

Loading…
Cancel
Save