Fix colour textures loading upside down and crashes on damaged PNGs

Colour height maps now displace the same way up as grayscale ones. A
truncated or corrupt PNG in the texture folder or a project file now
loads as an empty texture instead of aborting the app, and a 16-bit
colour PNG is converted on load rather than displacing as noise.
This commit is contained in:
SoftFever
2026-09-18 20:37:35 +08:00
parent 77bd987979
commit 3d7682e292
4 changed files with 94 additions and 20 deletions
+51 -1
View File
@@ -1,4 +1,6 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <catch2/catch_all.hpp>
#include <algorithm>
@@ -779,11 +781,27 @@ static std::shared_ptr<std::vector<unsigned char>> make_rgb_png_2x2()
return std::make_shared<std::vector<unsigned char>>(std::begin(bytes), std::end(bytes));
}
// The same image with an opaque alpha channel, so four bytes per pixel instead of three.
static std::shared_ptr<std::vector<unsigned char>> make_rgba_png_2x2()
{
static const unsigned char bytes[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
0x08, 0x06, 0x00, 0x00, 0x00, 0x72, 0xb6, 0x0d, 0x24, 0x00, 0x00, 0x00,
0x12, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xf8, 0xcf, 0xc0, 0xf0,
0x1f, 0x0c, 0x81, 0x34, 0x18, 0x00, 0x00, 0x49, 0xc8, 0x09, 0xf7, 0x03,
0xd9, 0x64, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
0x42, 0x60, 0x82,
};
return std::make_shared<std::vector<unsigned char>>(std::begin(bytes), std::end(bytes));
}
TEST_CASE("TextureDisplacement: a colour texture decodes to both colour and height", "[TextureDisplacement]")
{
TextureDisplacementLayer layer;
layer.slot = 0;
layer.image_data = make_rgb_png_2x2();
// Row stride differs between the two, and both must come out the same way up.
layer.image_data = GENERATE(make_rgb_png_2x2(), make_rgba_png_2x2());
const DecodedHeightTexture tex = decode_height_texture(layer);
REQUIRE_FALSE(tex.empty());
@@ -806,6 +824,38 @@ TEST_CASE("TextureDisplacement: a colour texture decodes to both colour and heig
CHECK(int(tex.pixels[3]) == 255); // white
}
TEST_CASE("A 16-bit colour texture decodes to nothing rather than noise", "[TextureDisplacement]")
{
// The 2x2 image above at 16 bits per channel. The texture library converts such a file to 8-bit
// on load, so it can only arrive here stored as-is, from a project file.
static const unsigned char bytes[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
0x10, 0x02, 0x00, 0x00, 0x00, 0xad, 0x44, 0x46, 0x30, 0x00, 0x00, 0x00,
0x12, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xf8, 0xff, 0x9f, 0x01,
0x0c, 0x60, 0x34, 0x90, 0x01, 0x01, 0x00, 0x75, 0xa4, 0x0b, 0xf5, 0x97,
0xf4, 0x36, 0xa1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
0x42, 0x60, 0x82,
};
TextureDisplacementLayer layer;
layer.slot = 0;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::begin(bytes), std::end(bytes));
CHECK(decode_height_texture(layer).empty());
}
TEST_CASE("A truncated texture decodes to nothing instead of aborting", "[TextureDisplacement]")
{
// A half-copied file in the texture folder, or a damaged project file. libpng reports this by
// longjmp, and aborts the process if the decoder has not set a jump buffer to land on.
const auto whole = GENERATE(make_flat_gray_png(128, 16, 16), make_rgb_png_2x2());
TextureDisplacementLayer layer;
layer.slot = 0;
layer.image_data = std::make_shared<std::vector<unsigned char>>(whole->begin(), whole->begin() + whole->size() / 2);
CHECK(decode_height_texture(layer).empty());
}
TEST_CASE("TextureDisplacement: a grayscale texture reports no colour", "[TextureDisplacement]")
{
// The shipped library is all grayscale, and has_color() is what the whole colour feature keys