Browse Source

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.
pull/601/head
Vladimír Vondruš 3 years ago
parent
commit
5796b597f3
  1. 2
      doc/changelog.dox
  2. 353
      src/MagnumPlugins/TgaImageConverter/Test/TgaImageConverterTest.cpp
  3. 10
      src/MagnumPlugins/TgaImageConverter/TgaImageConverter.conf
  4. 211
      src/MagnumPlugins/TgaImageConverter/TgaImageConverter.cpp
  5. 17
      src/MagnumPlugins/TgaImageConverter/TgaImageConverter.h

2
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

353
src/MagnumPlugins/TgaImageConverter/Test/TgaImageConverterTest.cpp

@ -30,15 +30,19 @@
#include <Corrade/Containers/String.h>
#include <Corrade/TestSuite/Tester.h>
#include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/TestSuite/Compare/Numeric.h>
#include <Corrade/Utility/ConfigurationGroup.h>
#include <Corrade/Utility/DebugStl.h> /** @todo remove once Debug is stream-free */
#include <Corrade/Utility/FormatStl.h>
#include <Corrade/Utility/Path.h>
#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<AbstractImporter> _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<char> data;
Containers::Array<UnsignedByte> expected;
Int width;
Containers::Optional<bool> 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<Containers::Array<char>> 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<AbstractImageConverter> 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<Containers::Array<char>> 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<AbstractImageConverter> converter = _converterManager.instantiate("TgaImageConverter");
/* Disable RLE, that's tested in rle*() instead */
converter->configuration().setValue("rle", false);
Containers::Optional<Containers::Array<char>> 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<AbstractImageConverter> converter = _converterManager.instantiate("TgaImageConverter");
if(data.rleAcrossScanlines)
converter->configuration().setValue("rleAcrossScanlines", *data.rleAcrossScanlines);
Containers::Optional<Containers::Array<char>> array = converter->convertToData(image);
CORRADE_VERIFY(array);
CORRADE_COMPARE_AS(
Containers::arrayCast<const UnsignedByte>(*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<AbstractImporter> importer = _importerManager.instantiate("TgaImporter");
CORRADE_VERIFY(importer->openData(*array));
Containers::Optional<Trade::ImageData2D> 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<AbstractImageConverter> converter = _converterManager.instantiate("TgaImageConverter");
Containers::Optional<Containers::Array<char>> array = converter->convertToData(image);
CORRADE_VERIFY(array);
CORRADE_COMPARE_AS(
Containers::arrayCast<const UnsignedByte>(*array)
.exceptPrefix(sizeof(Implementation::TgaHeader)),
Containers::arrayView<UnsignedByte>({
/* 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<AbstractImporter> importer = _importerManager.instantiate("TgaImporter");
CORRADE_VERIFY(importer->openData(*array));
Containers::Optional<Trade::ImageData2D> 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<const char>(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<AbstractImageConverter> converter = _converterManager.instantiate("TgaImageConverter");
Containers::Optional<Containers::Array<char>> array = converter->convertToData(image);
CORRADE_VERIFY(array);
CORRADE_COMPARE_AS(
Containers::arrayCast<const UnsignedByte>(*array)
.exceptPrefix(sizeof(Implementation::TgaHeader)),
Containers::arrayView<UnsignedByte>({
/* 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<AbstractImporter> importer = _importerManager.instantiate("TgaImporter");
CORRADE_VERIFY(importer->openData(*array));
Containers::Optional<Trade::ImageData2D> 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<const char>(RleRgbaData),
TestSuite::Compare::Container);
}
void TgaImageConverterTest::rleDisabled() {
ImageView2D image{PixelFormat::RGBA8Unorm, {4, 2}, RleRgbaData};
Containers::Pointer<AbstractImageConverter> converter = _converterManager.instantiate("TgaImageConverter");
converter->configuration().setValue("rle", false);
const Containers::Optional<Containers::Array<char>> array = converter->convertToData(image);
CORRADE_VERIFY(array);
CORRADE_COMPARE_AS(array->exceptPrefix(sizeof(Implementation::TgaHeader)),Containers::arrayCast<const char>(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);

10
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_]

211
src/MagnumPlugins/TgaImageConverter/TgaImageConverter.cpp

@ -28,9 +28,11 @@
#include <fstream>
#include <tuple>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/GrowableArray.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/String.h>
#include <Corrade/Utility/Algorithms.h>
#include <Corrade/Utility/ConfigurationGroup.h>
#include <Corrade/Utility/Endianness.h>
#include "Magnum/ImageView.h"
@ -58,15 +60,173 @@ Containers::String TgaImageConverter::doMimeType() const {
return "image/x-tga"_s;
}
template<class T> 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<class T> void rleEncode(Containers::Array<char>& 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<const T> pixels = image.pixels<T>();
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<std::size_t> 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<const T> 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<Containers::Array<char>> 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<char> data{NoInit, sizeof(Implementation::TgaHeader) + pixelSize*image.size().product()};
const bool rle = configuration().value<bool>("rle");
Containers::Array<char> data;
if(rle) {
arrayAppend(data, NoInit, sizeof(Implementation::TgaHeader));
} else {
data = Containers::Array<char>{NoInit, sizeof(Implementation::TgaHeader) + pixelSize*image.size().product()};
}
/* Clear the header and fill non-zero values */
auto& header = *reinterpret_cast<Implementation::TgaHeader*>(data.begin());
@ -93,17 +253,42 @@ Containers::Optional<Containers::Array<char>> 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<char> pixels = data.exceptPrefix(sizeof(Implementation::TgaHeader));
Utility::copy(image.pixels(), Containers::StridedArrayView3D<char>{pixels,
{std::size_t(image.size().y()), std::size_t(image.size().x()), pixelSize}});
if(image.format() == PixelFormat::RGB8Unorm) {
for(Vector3ub& pixel: Containers::arrayCast<Vector3ub>(pixels))
pixel = Math::gather<'b', 'g', 'r'>(pixel);
} else if(image.format() == PixelFormat::RGBA8Unorm) {
for(Vector4ub& pixel: Containers::arrayCast<Vector4ub>(pixels))
pixel = Math::gather<'b', 'g', 'r', 'a'>(pixel);
/* Perform RLE encoding */
if(rle) {
header.imageType |= 8;
const bool rleAcrossScanlines = configuration().value<bool>("rleAcrossScanlines");
switch(image.format()) {
case PixelFormat::R8Unorm:
rleEncode<UnsignedByte>(data, image, rleAcrossScanlines);
break;
case PixelFormat::RGB8Unorm:
rleEncode<Vector3ub>(data, image, rleAcrossScanlines);
break;
case PixelFormat::RGBA8Unorm:
rleEncode<Vector4ub>(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<char> pixels = data.exceptPrefix(sizeof(Implementation::TgaHeader));
Utility::copy(image.pixels(), Containers::StridedArrayView3D<char>{pixels,
{std::size_t(image.size().y()), std::size_t(image.size().x()), pixelSize}});
if(image.format() == PixelFormat::RGB8Unorm) {
for(Vector3ub& pixel: Containers::arrayCast<Vector3ub>(pixels))
pixel = Math::gather<'b', 'g', 'r'>(pixel);
} else if(image.format() == PixelFormat::RGBA8Unorm) {
for(Vector4ub& pixel: Containers::arrayCast<Vector4ub>(pixels))
pixel = Math::gather<'b', 'g', 'r', 'a'>(pixel);
}
}
/* GCC 4.8 needs extra help here */

17
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:

Loading…
Cancel
Save