Browse Source

png reader and tests

Kajetan Johannes Hammerle 3 years ago
parent
commit
bff2959fac

+ 9 - 1
Main.cpp

@@ -1,3 +1,5 @@
+#include <iostream>
+
 #include "tests/ArrayTests.h"
 #include "tests/HashMapTests.h"
 #include "tests/ListTests.h"
@@ -16,8 +18,13 @@
 #include "tests/UtilsTests.h"
 #include "tests/ColorTests.h"
 #include "tests/ClockTests.h"
+#include "tests/PNGReaderTests.h"
 
-int main() {
+int main(int argAmount, char** args) {
+    if(argAmount < 2) {
+        std::cout << "missing path to images\n";
+        return 0;
+    }
     ArrayTests::test();
     HashMapTests::test();
     ListTests::test();
@@ -36,5 +43,6 @@ int main() {
     UtilsTests::test();
     ColorTests::test();
     ClockTests::test();
+    PNGReaderTests::test(args[1]);
     return 0;
 }

+ 114 - 0
images/PNGReader.cpp

@@ -0,0 +1,114 @@
+#include <iostream>
+#include <cstring>
+#include <libpng16/png.h>
+
+#include "images/PNGReader.h"
+
+PNGReader::PNGReader(const char* path) : path(path), width(0), height(0), channels(0), bitDepth(0), rowBytes(0),
+file(fopen(path, "r")), read(nullptr), info(nullptr), rowPointers(nullptr) {
+    if(file == nullptr) {
+        std::cout << "file '" << path << "' cannot be read: " << strerror(errno) << "\n";
+        return;
+    }
+    if(checkSignature()) {
+        return;
+    }
+    read = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+    if(read == nullptr) {
+        std::cout << "cannot create png read data structure\n";
+        return;
+    }
+    info = png_create_info_struct(read);
+    if(info == nullptr) {
+        std::cout << "cannot create png info structure\n";
+        return;
+    }
+    if(setjmp(png_jmpbuf(read))) {
+        std::cout << "png file '" << path << "' has used error callback\n";
+        return;
+    }
+    png_init_io(read, file);
+    png_set_sig_bytes(read, 8);
+    png_read_info(read, info);
+
+    png_set_expand(read);
+
+    width = png_get_image_width(read, info);
+    height = png_get_image_height(read, info);
+    channels = png_get_channels(read, info);
+    bitDepth = png_get_bit_depth(read, info);
+    if(png_get_bit_depth(read, info) == 16) {
+        png_set_strip_16(read);
+    }
+    rowBytes = png_get_rowbytes(read, info);
+}
+
+PNGReader::~PNGReader() {
+    if(file != nullptr) {
+        fclose(file);
+    }
+    if(rowPointers != nullptr) {
+        png_free(read, rowPointers);
+    }
+    png_destroy_read_struct(&read, &info, nullptr);
+}
+
+int PNGReader::getWidth() const {
+    return width;
+}
+
+int PNGReader::getHeight() const {
+    return height;
+}
+
+int PNGReader::getChannels() const {
+    return channels;
+}
+
+int PNGReader::getBufferSize() const {
+    return width * height;
+}
+
+bool PNGReader::hasError() const {
+    if(channels < 1 || channels > 4) {
+        std::cout << "'" << path << "' has unsupported number of channels: " << channels << "\n";
+        return true;
+    } else if(width < 1 || width > 2048 || height < 1 || height > 2048) {
+        std::cout << "width and height of '" << path << "' are too big\n";
+        return true;
+    } else if(bitDepth != 8 && bitDepth != 16) {
+        std::cout << "bit depth of '" << path << "' is neither 8 or 16\n";
+        return true;
+    } else if(getBufferSize() * channels != (rowBytes * height * 8 / bitDepth)) {
+        std::cout << "'" << path << "' needs an unexpected buffer size\n";
+        return true;
+    }
+    return false;
+}
+
+bool PNGReader::checkSignature() {
+    png_byte buffer[8];
+    if(fread(buffer, sizeof (png_byte), 8, file) != 8) {
+        std::cout << "cannot read signature of file '" << path << "'\n";
+        return true;
+    }
+    if(png_sig_cmp(buffer, 0, 8)) {
+        std::cout << "file '" << path << "' is not a png\n";
+        return true;
+    }
+    return false;
+}
+
+bool PNGReader::readData(unsigned char* buffer) {
+    if(setjmp(png_jmpbuf(read))) {
+        std::cout << "png file '" << path << "' has used error callback\n";
+        return true;
+    }
+    rowPointers = static_cast<ColorChannel**> (png_malloc(read, height * (sizeof (ColorChannel*))));
+    for(int i = 0; i < height; i++) {
+        rowPointers[i] = (buffer + i * width * channels);
+    }
+    png_set_rows(read, info, reinterpret_cast<png_bytepp> (rowPointers));
+    png_read_image(read, reinterpret_cast<png_bytepp> (rowPointers));
+    return false;
+}

+ 47 - 0
images/PNGReader.h

@@ -0,0 +1,47 @@
+#ifndef PNGREADER_H
+#define PNGREADER_H
+
+#include <png.h>
+
+#include "utils/Color.h"
+
+class PNGReader final {
+    const char* path;
+    int width;
+    int height;
+    int channels;
+    int bitDepth;
+    int rowBytes;
+    FILE* file;
+    png_structp read;
+    png_infop info;
+    ColorChannel** rowPointers;
+
+public:
+    PNGReader(const char* path);
+    ~PNGReader();
+    PNGReader(const PNGReader& other) = delete;
+    PNGReader(PNGReader&& other) = delete;
+    PNGReader& operator=(const PNGReader& other) = delete;
+    PNGReader& operator=(PNGReader&& other) = delete;
+
+    int getWidth() const;
+    int getHeight() const;
+    int getChannels() const;
+    int getBufferSize() const;
+    bool hasError() const;
+
+    template<int N>
+    bool readData(Color<N>* buffer) {
+        if(channels != N) {
+            return true;
+        }
+        return readData(reinterpret_cast<ColorChannel*> (buffer));
+    }
+
+private:
+    bool readData(ColorChannel* buffer);
+    bool checkSignature();
+};
+
+#endif

+ 6 - 1
meson.build

@@ -27,8 +27,13 @@ sources = ['Main.cpp',
     'tests/UtilsTests.cpp',
     'tests/ColorTests.cpp',
     'utils/Clock.cpp',
-    'tests/ClockTests.cpp']
+    'tests/ClockTests.cpp',
+    'images/PNGReader.cpp',
+    'tests/PNGReaderTests.cpp']
+
+pngDep = dependency('libpng')
 
 executable('tests', 
     sources: sources,
+    dependencies : [pngDep],
     cpp_args: ['-Wall', '-Wextra', '-pedantic', '-Werror'])

+ 10 - 10
tests/ColorTests.cpp

@@ -6,23 +6,23 @@ const float eps = 0.0001f;
 
 static void testColor1(Test& test) {
     Color1 c = Color1(36);
-    test.checkEqual(static_cast<Color1::Channel>(36), c.data[0], "build 1 | 1");
+    test.checkEqual(static_cast<ColorChannel>(36), c.data[0], "build 1 | 1");
     test.checkFloat(36.0f / 255.0f, c.asFloat(0), eps, "build 1 | 2");
 }
 
 static void testColor2(Test& test) {
     Color2 c = Color2(36, 100);
-    test.checkEqual(static_cast<Color2::Channel>(36), c.data[0], "build 2 | 1");
-    test.checkEqual(static_cast<Color2::Channel>(100), c.data[1], "build 2 | 2");
+    test.checkEqual(static_cast<ColorChannel>(36), c.data[0], "build 2 | 1");
+    test.checkEqual(static_cast<ColorChannel>(100), c.data[1], "build 2 | 2");
     test.checkFloat(36.0f / 255.0f, c.asFloat(0), eps, "build 2 | 3");
     test.checkFloat(100.0f / 255.0f, c.asFloat(1), eps, "build 2 | 4");
 }
 
 static void testColor3(Test& test) {
     Color3 c = Color3(36, 100, 200);
-    test.checkEqual(static_cast<Color3::Channel>(36), c.data[0], "build 3 | 1");
-    test.checkEqual(static_cast<Color3::Channel>(100), c.data[1], "build 3 | 2");
-    test.checkEqual(static_cast<Color3::Channel>(200), c.data[2], "build 3 | 3");
+    test.checkEqual(static_cast<ColorChannel>(36), c.data[0], "build 3 | 1");
+    test.checkEqual(static_cast<ColorChannel>(100), c.data[1], "build 3 | 2");
+    test.checkEqual(static_cast<ColorChannel>(200), c.data[2], "build 3 | 3");
     test.checkFloat(36.0f / 255.0f, c.asFloat(0), eps, "build 3 | 4");
     test.checkFloat(100.0f / 255.0f, c.asFloat(1), eps, "build 3 | 5");
     test.checkFloat(200.0f / 255.0f, c.asFloat(2), eps, "build 3 | 6");
@@ -30,10 +30,10 @@ static void testColor3(Test& test) {
 
 static void testColor4(Test& test) {
     Color4 c = Color4(36, 100, 200, 142);
-    test.checkEqual(static_cast<Color4::Channel>(36), c.data[0], "build 4 | 1");
-    test.checkEqual(static_cast<Color4::Channel>(100), c.data[1], "build 4 | 2");
-    test.checkEqual(static_cast<Color4::Channel>(200), c.data[2], "build 4 | 3");
-    test.checkEqual(static_cast<Color4::Channel>(142), c.data[3], "build 4 | 4");
+    test.checkEqual(static_cast<ColorChannel>(36), c.data[0], "build 4 | 1");
+    test.checkEqual(static_cast<ColorChannel>(100), c.data[1], "build 4 | 2");
+    test.checkEqual(static_cast<ColorChannel>(200), c.data[2], "build 4 | 3");
+    test.checkEqual(static_cast<ColorChannel>(142), c.data[3], "build 4 | 4");
     test.checkFloat(36.0f / 255.0f, c.asFloat(0), eps, "build 4 | 5");
     test.checkFloat(100.0f / 255.0f, c.asFloat(1), eps, "build 4 | 6");
     test.checkFloat(200.0f / 255.0f, c.asFloat(2), eps, "build 4 | 7");

+ 134 - 0
tests/PNGReaderTests.cpp

@@ -0,0 +1,134 @@
+#include "tests/PNGReaderTests.h"
+#include "tests/Test.h"
+#include "images/PNGReader.h"
+#include "utils/StringBuffer.h"
+
+template<int N>
+static void testRead(int correct, Test& test, PNGReader& png, const char* text) {
+    Color<N>* buffer = new Color<N>[png.getBufferSize()];
+    test.checkEqual(N != correct, png.readData(buffer), text);
+    delete[] buffer;
+}
+
+static void testFullRead(int correct, Test& test, PNGReader& png, const char* text) {
+    testRead<1>(correct, test, png, text);
+    testRead<2>(correct, test, png, text);
+    testRead<3>(correct, test, png, text);
+    testRead<4>(correct, test, png, text);
+}
+
+static void testReadRGB8(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("rgb8.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read rgb8 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "rgb8 width");
+    test.checkEqual(64, png.getHeight(), "rgb8 height");
+    test.checkEqual(3, png.getChannels(), "rgb8 channels");
+    testFullRead(3, test, png, "rgb8 read");
+}
+
+static void testReadRGB16(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("rgb16.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read rgb16 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "rgb16 width");
+    test.checkEqual(64, png.getHeight(), "rgb16 height");
+    test.checkEqual(3, png.getChannels(), "rgb16 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "rgb16 channels");
+    testFullRead(3, test, png, "rgb16 read");
+}
+
+static void testReadRGBA8(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("rgba8.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read rgba8 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "rgba8 width");
+    test.checkEqual(64, png.getHeight(), "rgba8 height");
+    test.checkEqual(4, png.getChannels(), "rgba8 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "rgba8 channels");
+    testFullRead(4, test, png, "rgba8 read");
+}
+
+static void testReadRGBA16(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("rgba16.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read rgba16 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "rgba16 width");
+    test.checkEqual(64, png.getHeight(), "rgba16 height");
+    test.checkEqual(4, png.getChannels(), "rgba16 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "rgba16 channels");
+    testFullRead(4, test, png, "rgba16 read");
+}
+
+static void testReadGray8(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("gray8.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read gray8 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "gray8 width");
+    test.checkEqual(64, png.getHeight(), "gray8 height");
+    test.checkEqual(1, png.getChannels(), "gray8 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "gray8 channels");
+    testFullRead(1, test, png, "gray8 read");
+}
+
+static void testReadGray16(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("gray16.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read gray16 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "gray16 width");
+    test.checkEqual(64, png.getHeight(), "gray16 height");
+    test.checkEqual(1, png.getChannels(), "gray16 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "gray16 channels");
+    testFullRead(1, test, png, "gray16 read");
+}
+
+static void testReadGrayA8(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("graya8.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read graya8 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "graya8 width");
+    test.checkEqual(64, png.getHeight(), "graya8 height");
+    test.checkEqual(2, png.getChannels(), "graya8 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "graya8 channels");
+    testFullRead(2, test, png, "graya8 read");
+}
+
+static void testReadGrayA16(Test& test, const char* path) {
+    PNGReader png(StringBuffer<200>(path).append("graya16.png"));
+    if(png.hasError()) {
+        test.checkEqual(false, true, "read graya16 error");
+        return;
+    }
+    test.checkEqual(32, png.getWidth(), "graya16 width");
+    test.checkEqual(64, png.getHeight(), "graya16 height");
+    test.checkEqual(2, png.getChannels(), "graya16 channels");
+    test.checkEqual(32 * 64, png.getBufferSize(), "graya16 channels");
+    testFullRead(2, test, png, "graya16 read");
+}
+
+void PNGReaderTests::test(const char* path) {
+    Test test("PNGReader");
+    testReadRGB8(test, path);
+    testReadRGB16(test, path);
+    testReadRGBA8(test, path);
+    testReadRGBA16(test, path);
+    testReadGray8(test, path);
+    testReadGray16(test, path);
+    testReadGrayA8(test, path);
+    testReadGrayA16(test, path);
+    test.finalize();
+}

+ 8 - 0
tests/PNGReaderTests.h

@@ -0,0 +1,8 @@
+#ifndef PNGREADERTESTS_H
+#define PNGREADERTESTS_H
+
+namespace PNGReaderTests {
+    void test(const char* path);
+}
+
+#endif

BIN
tests/resources/gray16.png


BIN
tests/resources/gray8.png


BIN
tests/resources/graya16.png


BIN
tests/resources/graya8.png


BIN
tests/resources/rgb16.png


BIN
tests/resources/rgb8.png


BIN
tests/resources/rgba16.png


BIN
tests/resources/rgba8.png


+ 6 - 3
utils/Color.h

@@ -1,15 +1,18 @@
 #ifndef COLOR_H
 #define COLOR_H
 
+typedef unsigned char ColorChannel;
+
 template<int N>
 struct Color {
     static_assert(N <= 4, "a color can have at most 4 channels");
-    typedef unsigned char Channel;
 
-    Channel data[N];
+    ColorChannel data[N];
+
+    Color() = default;
 
     template<typename... Args>
-    Color(Channel a, Args&&... args) {
+    Color(ColorChannel a, Args&&... args) {
         const int size = sizeof...(args) + 1;
         int init[size] = {a, args...};
         static_assert(N == size, "color size and amount of channel arguments do not match");