diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7930220..d6bfd50 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(common) add_subdirectory(bmp) +add_subdirectory(png) add_subdirectory(qoi) add_library(image formats.hpp formats.cpp) @@ -9,5 +10,6 @@ target_link_libraries(image common bmp + png qoi ) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 65a4268..79a686f 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -1,6 +1,7 @@ set(HEADERS bits.hpp decoder.hpp + deflate_compressor.hpp exceptions.hpp format.hpp log.hpp @@ -10,6 +11,8 @@ set(HEADERS ) set(SOURCES + bits.cpp + deflate_compressor.cpp pixel.cpp raw_image.cpp stream.cpp diff --git a/src/common/bits.cpp b/src/common/bits.cpp new file mode 100644 index 0000000..e5c676d --- /dev/null +++ b/src/common/bits.cpp @@ -0,0 +1,9 @@ +#include "bits.hpp" + +uint8_t reverse_byte(uint8_t value) +{ + value = (value & 0b11110000) >> 4 | (value & 0b00001111) << 4; + value = (value & 0b11001100) >> 2 | (value & 0b00110011) << 2; + value = (value & 0b10101010) >> 1 | (value & 0b01010101) << 1; + return value; +} diff --git a/src/common/bits.hpp b/src/common/bits.hpp index d3cfb6a..5f98f56 100644 --- a/src/common/bits.hpp +++ b/src/common/bits.hpp @@ -29,3 +29,5 @@ T generate_bitmask(uint8_t number_of_set_bits) { return (1 << number_of_set_bits) - 1; } + +uint8_t reverse_byte(uint8_t value); diff --git a/src/common/deflate_compressor.cpp b/src/common/deflate_compressor.cpp new file mode 100644 index 0000000..f5fd8b4 --- /dev/null +++ b/src/common/deflate_compressor.cpp @@ -0,0 +1,318 @@ +#include "deflate_compressor.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "bits.hpp" +#include "stream.hpp" + +namespace Compression { + +const std::vector DeflateTables::extra_lengths = { + { 257, 0, 3 }, + { 258, 0, 4 }, + { 259, 0, 5 }, + { 260, 0, 6 }, + { 261, 0, 7 }, + { 262, 0, 8 }, + { 263, 0, 9 }, + { 264, 0, 10 }, + { 265, 1, 11 }, + { 266, 1, 13 }, + { 267, 1, 15 }, + { 268, 1, 17 }, + { 269, 2, 19 }, + { 270, 2, 23 }, + { 271, 2, 27 }, + { 272, 2, 31 }, + { 273, 3, 35 }, + { 274, 3, 43 }, + { 275, 3, 51 }, + { 276, 3, 59 }, + { 277, 4, 67 }, + { 278, 4, 83 }, + { 279, 4, 99 }, + { 280, 4, 115 }, + { 281, 5, 131 }, + { 282, 5, 163 }, + { 283, 5, 195 }, + { 284, 5, 227 }, + { 285, 0, 258 }, +}; + +const std::vector DeflateTables::extra_distances = { + { 0, 0, 1 }, + { 1, 0, 2 }, + { 2, 0, 3 }, + { 3, 0, 4 }, + { 4, 1, 5 }, + { 5, 1, 7 }, + { 6, 2, 9 }, + { 7, 2, 13 }, + { 8, 3, 17 }, + { 9, 3, 25 }, + { 10, 4, 33 }, + { 11, 4, 49 }, + { 12, 5, 65 }, + { 13, 5, 97 }, + { 14, 6, 129 }, + { 15, 6, 193 }, + { 16, 7, 257 }, + { 17, 7, 385 }, + { 18, 8, 513 }, + { 19, 8, 769 }, + { 20, 9, 1025 }, + { 21, 9, 1537 }, + { 22, 10, 2049 }, + { 23, 10, 3073 }, + { 24, 11, 4097 }, + { 25, 11, 6145 }, + { 26, 12, 8193 }, + { 27, 12, 12289 }, + { 28, 13, 16385 }, + { 29, 13, 24577 }, +}; + +DeflateTables::ExtraLength DeflateTables::get_extra_length(uint16_t code) +{ + for (auto it = extra_lengths.begin(); it != extra_lengths.end(); ++it) + if (it->code == code) return *it; + + throw std::runtime_error { "Code not found in get_extra_length()." }; +} + +DeflateTables::ExtraDistance DeflateTables::get_extra_distance(uint16_t code) +{ + for (auto it = extra_distances.begin(); it != extra_distances.end(); ++it) + if (it->code == code) return *it; + + throw std::runtime_error { "Code not found in get_extra_distance()." }; +} + +HuffmanNode::HuffmanNode(int length, int symbol) : + length { length }, + symbol { symbol } +{ +} + +HuffmanTree::HuffmanTree(std::vector> nodes) +{ + // Build code_to_length + length_count dictionnary + int max_length = 0; + std::map code_to_length; + std::map length_count; + for (auto it = nodes.begin(); it != nodes.end(); ++it) + { + code_to_length[(*it)->symbol] = (*it)->length; + if ((*it)->length > max_length) max_length = (*it)->length; + ++length_count[(*it)->length]; + } + + // Build next_code dictionnary + length_count[0] = 0; + std::map next_code; + uint32_t code = 0, max_code = 0; + for (int length = 1; length <= max_length; ++length) + { + code = (code + length_count[length - 1]) << 1; + if (code > max_code) max_code = code; + next_code[length] = code; + } + + // Build final table + for (uint32_t i = 0; i < nodes.size(); ++i) + { + int length = nodes[i]->length; + if (length != 0) + { + nodes[i]->code = next_code[length]; + next_code[length]++; + } + } + + for (auto it = nodes.begin(); it != nodes.end(); ++it) + code_to_symbol[(*it)->code] = (*it)->symbol; +} + +int HuffmanTree::get_value(InputStream& in) +{ + if (in.remaining_size() == 0) + throw std::runtime_error { "HuffmanTree::get_value() cannot run with an empty InputStream." }; + + uint32_t read_value = 0; + std::map::iterator it; + do + { + read_value = (read_value << 1) | in.read_u1_le(); + it = code_to_symbol.find(read_value); + } while (it == code_to_symbol.end()); + return it->second; +} + +DeflateCompressor::DeflateCompressor(InputStream& in) : + in { in } +{ +} + +std::vector DeflateCompressor::uncompress() +{ + out.clear(); + + const uint8_t method = in.read_bits_le(4); + const int8_t info = in.read_bits_le(4); + if (method != 8) + throw std::runtime_error { "Cannot deflate with a method of " + std::to_string(method) + "." }; + + in.read_bits_le(5); + uint8_t fdict = in.read_bits_le(1); + uint8_t flevel = in.read_bits_le(2); + + bool is_last_block = false; + do + { + is_last_block = in.read_bits_le(1); + uint8_t compression_level = in.read_bits_le(2); + read_block(compression_level); + + // TODO: Check ALDER32 + in.skip(4); + } while (!is_last_block); + + return out; +} + +void DeflateCompressor::read_block(uint8_t compression_level) +{ + switch (compression_level) + { + case 0: + read_block_compression_level_0(); + break; + case 1: + read_block_compression_level_1(); + break; + case 2: + read_block_compression_level_2(); + break; + default: + throw std::runtime_error { "The compression level " + std::to_string(compression_level) + " cannot be decompressed." }; + } +} + +void DeflateCompressor::read_block_compression_level_0() +{ + std::cout << "LEVEL 0" << std::endl; + + uint16_t length = in.read_u16_le(); + uint16_t length_inverted = in.read_u16_le(); + + if (length != (uint16_t)~length_inverted) + throw std::runtime_error { std::to_string(length) + " and " + std::to_string(~length_inverted) + " are not equals. Stream integrity error." }; + + for (int i = 0; i < length; ++i) + out.push_back(in.read_u8()); +} + +void DeflateCompressor::read_block_compression_level_1() +{ + std::cout << "LEVEL 1" << std::endl; + + std::vector> length_nodes {}; + std::vector> distance_nodes {}; + + length_nodes.reserve(288); + distance_nodes.reserve(32); + + for (int i = 0; i <= 287; ++i) + length_nodes.push_back(std::make_shared(size_of_code(i), i)); + + for (int i = 0; i <= 31; ++i) + distance_nodes.push_back(std::make_shared(5, i)); + + HuffmanTree length_tree { length_nodes }; + HuffmanTree distance_tree { distance_nodes }; + + BufferStream stream; + stream.write_u8(0b11111101); + std::cout << length_tree.get_value(stream) << std::endl; + // SHOULD BE 143 + + throw std::exception(); + + bool loop = true; + while (loop) + { + std::cout << "========== (" << in.remaining_size() << ") ==========" << std::endl; + + int length_code = length_tree.get_value(in); + + std::cout << "CODE | " << length_code << std::endl; + + if (length_code == 256) + loop = false; + else if (length_code <= 255) + out.push_back((uint8_t)length_code); + else + { + std::cout << "REPEAT" << std::endl; + DeflateTables::ExtraLength length = DeflateTables::get_extra_length(length_code); + uint8_t length_additional_bits = length.bits; + int length_value = length.length << length_additional_bits; + length_value += in.read_bits_le(length_additional_bits); + + std::cout << "LENGTH: " << length_value << std::endl; + + uint16_t distance_code = distance_tree.get_value(in); + + DeflateTables::ExtraDistance distance = DeflateTables::get_extra_distance(distance_code); + uint8_t distance_additional_bits = distance.bits; + int distance_value = distance.distance << distance_additional_bits; + distance_value += in.read_bits_le(distance_additional_bits); + + std::cout << "DISTANCE: " << distance_value << std::endl; + + auto start = out.end() - distance_value; + auto end = start + length_value; + if (end > out.end()) end = out.end(); + std::vector repeated { start, end }; + + std::string s { repeated.begin(), repeated.end() }; + std::cout << "REPEATED: " << s << std::endl; + + // TODO: MISSING REPETITION + out.insert(out.end(), repeated.begin(), repeated.end()); + } + std::string s { out.begin(), out.end() }; + std::cout << "OUTPUT | " << s << std::endl; + } +} + +void DeflateCompressor::read_block_compression_level_2() +{ + std::cout << "LEVEL 2" << std::endl; +} + +uint8_t DeflateCompressor::size_of_code(uint16_t code) const +{ + if (code < 0) + throw std::runtime_error { "DeflateCompressor::size_of_code(): Code cannot be negative (" + std::to_string(code) + ")" }; + + if (code <= 143) + return 8; + if (code <= 255) + return 9; + if (code <= 279) + return 7; + if (code <= 287) + return 8; + + throw std::runtime_error { std::to_string(code) + " is not a valid code." }; +} +} diff --git a/src/common/deflate_compressor.hpp b/src/common/deflate_compressor.hpp new file mode 100644 index 0000000..407363f --- /dev/null +++ b/src/common/deflate_compressor.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +#include "stream.hpp" + +namespace Compression { + +struct DeflateTables +{ + struct ExtraLength + { + uint16_t code; + uint8_t bits; + uint16_t length; + }; + + struct ExtraDistance + { + uint16_t code; + uint8_t bits; + uint16_t distance; + }; + + static const std::vector extra_lengths; + static const std::vector extra_distances; + + static ExtraLength get_extra_length(uint16_t code); + static ExtraDistance get_extra_distance(uint16_t code); +}; + +struct HuffmanNode +{ + int length = 0; + int symbol = 0; + int code = 0; + + HuffmanNode(int length = 0, int symbol = 0); +}; + +class HuffmanTree +{ +private: + std::map code_to_symbol = std::map(); + +public: + HuffmanTree(std::vector> nodes); + + int get_value(InputStream& in); +}; + +class DeflateCompressor +{ +private: + InputStream& in; + std::vector out; + +public: + DeflateCompressor(InputStream& in); + + std::vector uncompress(); + +private: + void read_block(uint8_t compression_level); + void read_block_compression_level_0(); + void read_block_compression_level_1(); + void read_block_compression_level_2(); + + uint8_t size_of_code(uint16_t code) const; +}; + +} diff --git a/src/common/stream.cpp b/src/common/stream.cpp index d41d91a..237ca54 100644 --- a/src/common/stream.cpp +++ b/src/common/stream.cpp @@ -2,12 +2,27 @@ #include #include +#include #include #include #include #include "bits.hpp" +uint8_t InputStream::read_u1() +{ + uint8_t value = peek_u1(); + goto_next_bit(); + return value; +} + +uint8_t InputStream::read_u1_le() +{ + uint8_t value = peek_u1_le(); + goto_next_bit(); + return value; +} + uint8_t InputStream::peek_u8() { goto_complete_byte(); @@ -34,11 +49,19 @@ int8_t InputStream::read_i8() void InputStream::go_back(uint8_t n_bytes) { + current_bit = 0; get_input_stream().seekg(-n_bytes, std::ios::cur); } +void InputStream::goto_next_bit() +{ + if (++current_bit == 8) + goto_complete_byte(); +} + void InputStream::skip(uint8_t n_bytes) { + current_bit = 0; get_input_stream().seekg(n_bytes, std::ios::cur); } @@ -46,23 +69,38 @@ void InputStream::goto_complete_byte() { if (current_bit == 0) return; skip(1); +} + +void InputStream::goto_start() +{ + current_bit = 0; + get_input_stream().seekg(0, std::ios::beg); +} + +void InputStream::goto_end() +{ current_bit = 0; + get_input_stream().seekg(0, std::ios::end); } long InputStream::size() { // TODO: Handle tellg error (if it returns -1). + int current_bit = this->current_bit; + std::istream& input_stream = get_input_stream(); long current_position = input_stream.tellg(); - input_stream.seekg(0, std::ios::beg); + goto_start(); long begin = input_stream.tellg(); - input_stream.seekg(0, std::ios::end); + goto_end(); long end = input_stream.tellg(); input_stream.seekg(current_position); + this->current_bit = current_bit; + return end - begin; } @@ -70,42 +108,18 @@ long InputStream::remaining_size() { // TODO: Handle tellg error (if it returns -1). + int current_bit = this->current_bit; + std::istream& input_stream = get_input_stream(); long current_position = input_stream.tellg(); - input_stream.seekg(0, std::ios::end); + goto_end(); long end = input_stream.tellg(); input_stream.seekg(current_position); - return end - current_position; -} + this->current_bit = current_bit; -uint8_t InputStream::read_bits(uint8_t n_bits) -{ - assert(n_bits <= 8); - - uint8_t value = 0; - for (uint8_t i = 0; i < n_bits; ++i) - { - value = (value << 1) + peek_u1(); - if (++current_bit == 8) - goto_complete_byte(); - } - return value; -} - -uint8_t InputStream::read_bits_le(uint8_t n_bits) -{ - assert(n_bits <= 8); - - uint8_t value = 0; - for (uint8_t i = 0; i < n_bits; ++i) - { - value = (value << 1) + peek_u1_le(); - if (++current_bit == 8) - goto_complete_byte(); - } - return value; + return end - current_position; } InputFileStream::InputFileStream(const std::string& filename) : diff --git a/src/common/stream.hpp b/src/common/stream.hpp index be6385f..78e4d91 100644 --- a/src/common/stream.hpp +++ b/src/common/stream.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -14,9 +15,9 @@ class InputStream public: uint8_t peek_u1() { return (get_input_stream().peek() & (0b10000000 >> current_bit)) >> (7 - current_bit); } - uint8_t peek_u1_le() { return (get_input_stream().peek() >> current_bit) & 1; } - uint8_t read_u1() { return read_bits(1); } - uint8_t read_u1_le() { return read_bits_le(1); } + uint8_t peek_u1_le() { return (get_input_stream().peek() >> current_bit) & 0b00000001; } + uint8_t read_u1(); + uint8_t read_u1_le(); uint8_t peek_u8(); uint8_t read_u8(); @@ -53,6 +54,9 @@ class InputStream void go_back(uint8_t n_bytes = 1); void skip(uint8_t n_bytes = 1); void goto_complete_byte(); + void goto_next_bit(); + void goto_start(); + void goto_end(); long size(); long remaining_size(); @@ -95,8 +99,25 @@ class InputStream return value; } - uint8_t read_bits(uint8_t n_bits); - uint8_t read_bits_le(uint8_t n_bits); + template + T read_bits(uint8_t n_bits) + { + T value = 0; + for (uint8_t i = 0; i < n_bits; ++i) + value = (value << 1) | read_u1(); + + return value; + } + + template + T read_bits_le(uint8_t n_bits) + { + T value = 0; + for (int8_t i = 0; i < n_bits; ++i) + value |= read_u1_le() << i; + + return value; + } }; class OutputStream @@ -140,7 +161,7 @@ class OutputStream class BufferStream : public InputStream, public OutputStream { private: - std::stringstream stream; + std::stringstream stream { std::ios_base::binary | std::ios_base::in | std::ios_base::out }; public: void clear() { stream.clear(); } diff --git a/src/formats.cpp b/src/formats.cpp index 36678f5..62e5af9 100644 --- a/src/formats.cpp +++ b/src/formats.cpp @@ -3,11 +3,13 @@ #include #include "bmp_format.hpp" +#include "png_format.hpp" #include "qoi_format.hpp" const std::unordered_map Formats::FORMATS { { "qoi", QOI_FORMAT }, { "bmp", BMP_FORMAT }, + { "png", PNG_FORMAT }, }; const Format& Formats::get_by_id(const std::string& format_id) diff --git a/src/png/CMakeLists.txt b/src/png/CMakeLists.txt new file mode 100644 index 0000000..4555d4b --- /dev/null +++ b/src/png/CMakeLists.txt @@ -0,0 +1,20 @@ +set(HEADERS + png_common.hpp + png_decoder.hpp + png_format.hpp + png_encoder.hpp + png_settings.hpp +) + +set(SOURCES + png_common.cpp + png_decoder.cpp + png_encoder.cpp +) + +add_library(png ${HEADERS} ${SOURCES}) +target_include_directories(png PUBLIC ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(png common) + +find_package(ZLIB REQUIRED) +target_link_libraries(png ZLIB::ZLIB) diff --git a/src/png/png_common.cpp b/src/png/png_common.cpp new file mode 100644 index 0000000..632fd6c --- /dev/null +++ b/src/png/png_common.cpp @@ -0,0 +1 @@ +#include "png_common.hpp" diff --git a/src/png/png_common.hpp b/src/png/png_common.hpp new file mode 100644 index 0000000..ba2f567 --- /dev/null +++ b/src/png/png_common.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include +#include + +#include "pixel.hpp" +#include "raw_image.hpp" +#include "stream.hpp" + +namespace PNG { + +static constexpr uint64_t SIGNATURE = 0x89504E470D0A1A0A; + +// clang-format off +static constexpr uint32_t CHUNK_NAME_HEADER = 0x49484452; // IHDR +static constexpr uint32_t CHUNK_NAME_PALETTE = 0x504C5445; // PLTE +static constexpr uint32_t CHUNK_NAME_IMAGE_DATA = 0x49444154; // IDAT +static constexpr uint32_t CHUNK_NAME_IMAGE_TRAILER = 0x49454E44; // IEND + +static constexpr uint32_t CHUNK_NAME_PRIMARY_CHROMA_AND_WHITE_POINTS = 0x6348524D; // cHRM +static constexpr uint32_t CHUNK_NAME_IMAGE_GAMMA = 0x67414D41; // gAMA +static constexpr uint32_t CHUNK_NAME_ICC_PROFILE = 0x69434350; // iCCP +static constexpr uint32_t CHUNK_NAME_SIGNIFICANT_BIT = 0x73424954; // sBIT +static constexpr uint32_t CHUNK_NAME_STANDARD_RGB = 0x73524742; // sRGB +static constexpr uint32_t CHUNK_NAME_BACKGROUND_COLOR = 0x624B4744; // bKGD +static constexpr uint32_t CHUNK_NAME_IMAGE_HISTOGRAM = 0x68495354; // hIST +static constexpr uint32_t CHUNK_NAME_TRANSPARENCY = 0x74524E53; // tRNS +static constexpr uint32_t CHUNK_NAME_PHYSICAL_PIXEL_DIMENSIONS = 0x70485973; // pHYs +static constexpr uint32_t CHUNK_NAME_SUGGESTED_PALETTE = 0x73504C54; // sPLT +static constexpr uint32_t CHUNK_NAME_IMAGE_LAST_MODIFICATION_TIME = 0x74494D45; // tIME +static constexpr uint32_t CHUNK_NAME_INTERNATIONAL_TEXTUAL_DATA = 0x69545874; // iTXt +static constexpr uint32_t CHUNK_NAME_TEXTUAL_DATA = 0x74455874; // tEXt +static constexpr uint32_t CHUNK_NAME_COMPRESSED_TEXTUAL_DATA = 0x7A545874; // zTXt +// clang-format on + +enum class BitDepth : uint8_t +{ + BD_1 = 1, + BD_2 = 2, + BD_4 = 4, + BD_8 = 8, + BD_16 = 16, +}; + +enum class ColorType : uint8_t +{ + GREYSCALE = 0, + TRUE_COLOR = 2, + INDEXED_COLOR = 3, + GREYSCALE_WITH_ALPHA = 4, + TRUE_COLOR_WITH_ALPHA = 6, +}; + +enum class CompressionMethod : uint8_t +{ + DEFLATE_INFLATE = 0, +}; + +enum class FilterMethod : uint8_t +{ + ADAPTIVE = 0, +}; + +enum class FilterType : uint8_t +{ + NONE = 0, + SUB = 1, + UP = 2, + AVERAGE = 3, + PAETH = 4, +}; + +enum class InterlaceMethod : uint8_t +{ + STANDARD = 0, + ADAM_7 = 1, +}; + +enum class RenderingIntent : uint8_t +{ + PERCEPTUAL = 0, + RELATIVE_COLORIMETRIC = 1, + SATURATION = 2, + ABSOLUTE_COLORIMETRIC = 3, +}; + +enum class UnitSpecifier : uint8_t +{ + UNKNOWN = 0, + METER = 1, +}; + +struct Date +{ + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; +}; + +class TextualData +{ +protected: + std::string keyword; + std::string text; + +public: + TextualData(const std::string& keyword, const std::string& text) : + keyword { keyword }, text { text } {} + + virtual ~TextualData() = default; + + const std::string& get_keyword() const { return keyword; } + virtual const std::string& get_text() const { return text; } +}; + +class CompressedTextualData : public TextualData +{ +protected: + uint8_t compression_method; + +public: + CompressedTextualData(const std::string& keyword, const std::string& text, uint8_t compression_method) : + TextualData { keyword, text }, compression_method { compression_method } {} + + virtual ~CompressedTextualData() = default; + + // TODO: Uncompress text + const std::string& get_text() const override { return text; } +}; + +struct InternationalTextualData : public CompressedTextualData +{ +protected: + std::string language_tag; + uint8_t compression_flag; + +public: + InternationalTextualData(const std::string& keyword, const std::string& text, uint8_t compression_flag, uint8_t compression_method, const std::string& language_tag) : + CompressedTextualData { keyword, text, compression_method }, + language_tag { language_tag }, + compression_flag { compression_flag } + { + } + + virtual ~InternationalTextualData() = default; + + const std::string& get_language_tag() const { return language_tag; } + uint8_t get_compression_flag() const { return compression_flag; } +}; + +} diff --git a/src/png/png_decoder.cpp b/src/png/png_decoder.cpp new file mode 100644 index 0000000..02d7421 --- /dev/null +++ b/src/png/png_decoder.cpp @@ -0,0 +1,411 @@ +#include "png_decoder.hpp" + +#include +#include +#include +#include + +#include "bits.hpp" +#include "deflate_compressor.hpp" +#include "exceptions.hpp" +#include "log.hpp" +#include "pixel.hpp" +#include "png_common.hpp" + +namespace PNG { + +bool Decoder::can_decode(InputStream &in) +{ + return in.peek(8) == SIGNATURE; +} + +void Decoder::decode() +{ + // Signature + in.read(8); + + while (in.remaining_size() > 0) + decode_chunk(); + + uncompress_image_data(); + unfilter_image_data(); + unserialize_scanlines(); +} + +void Decoder::decode_chunk() +{ + const uint32_t chunk_size = in.read_u32(); + const uint32_t chunk_name = in.read_u32(); + switch (chunk_name) + { + case CHUNK_NAME_HEADER: + decode_chunk_header(chunk_size); + break; + case CHUNK_NAME_PALETTE: + decode_chunk_palette(chunk_size); + break; + case CHUNK_NAME_IMAGE_DATA: + decode_chunk_image_data(chunk_size); + break; + case CHUNK_NAME_IMAGE_TRAILER: + decode_chunk_image_trailer(chunk_size); + break; + case CHUNK_NAME_PRIMARY_CHROMA_AND_WHITE_POINTS: + decode_chunk_primary_chroma_and_white_points(chunk_size); + break; + case CHUNK_NAME_IMAGE_GAMMA: + decode_chunk_image_gamma(chunk_size); + break; + case CHUNK_NAME_ICC_PROFILE: + decode_chunk_icc_profile(chunk_size); + break; + case CHUNK_NAME_SIGNIFICANT_BIT: + decode_chunk_significant_bit(chunk_size); + break; + case CHUNK_NAME_STANDARD_RGB: + decode_chunk_standard_rgb(chunk_size); + break; + case CHUNK_NAME_BACKGROUND_COLOR: + decode_chunk_background_color(chunk_size); + break; + case CHUNK_NAME_IMAGE_HISTOGRAM: + decode_chunk_image_histogram(chunk_size); + break; + case CHUNK_NAME_TRANSPARENCY: + decode_chunk_transparency(chunk_size); + break; + case CHUNK_NAME_PHYSICAL_PIXEL_DIMENSIONS: + decode_chunk_physical_pixel_dimensions(chunk_size); + break; + case CHUNK_NAME_SUGGESTED_PALETTE: + decode_chunk_suggested_palette(chunk_size); + break; + case CHUNK_NAME_IMAGE_LAST_MODIFICATION_TIME: + decode_chunk_image_last_modification_time(chunk_size); + break; + case CHUNK_NAME_INTERNATIONAL_TEXTUAL_DATA: + decode_chunk_international_textual_data(chunk_size); + break; + case CHUNK_NAME_TEXTUAL_DATA: + decode_chunk_textual_data(chunk_size); + break; + case CHUNK_NAME_COMPRESSED_TEXTUAL_DATA: + decode_chunk_compressed_textual_data(chunk_size); + break; + default: + // LOG_ERROR("This " << std::hex << chunk_name << " chunk is not handled by the PNG decoder."); + in.skip(chunk_size); + } + + // TODO: Check CRC (uint32_t) + in.skip(4); // Cyclic redundancy code (CRC) +} + +void Decoder::decode_chunk_header(uint32_t chunk_size) +{ + if (chunk_size != 13) + throw std::runtime_error { "A chunk size different than 13 bytes is not supported." }; + + image.width = in.read_u32(); + image.height = in.read_u32(); + settings.bit_depth = (BitDepth)in.read_u8(); + settings.color_type = (ColorType)in.read_u8(); + settings.compression_method = (CompressionMethod)in.read_u8(); + settings.filter_method = (FilterMethod)in.read_u8(); + settings.interlace_method = (InterlaceMethod)in.read_u8(); +} + +void Decoder::decode_chunk_palette(uint32_t chunk_size) +{ + if (chunk_size % 3 != 0) + throw std::runtime_error { "Palette chunk size must be divisible by 3. chunk_size = " + std::to_string(chunk_size) }; + + for (uint8_t i = 0; i < chunk_size / 3; ++i) + { + settings.palette.clear(); + settings.palette.push_back(Pixel( + in.read_u8(), + in.read_u8(), + in.read_u8())); + } +} + +void Decoder::decode_chunk_image_data(uint32_t chunk_size) +{ + for (long i = 0; i < chunk_size; ++i) + compressed_image_data.write_u8(in.read_u8()); +} + +void Decoder::decode_chunk_image_trailer(uint32_t chunk_size) +{ + assert_chunk_size(0, chunk_size, "Image gamma"); +} + +void Decoder::decode_chunk_primary_chroma_and_white_points(uint32_t chunk_size) +{ + assert_chunk_size(32, chunk_size, "Primary chroma and white points"); + + const double DIVISION_FACTOR = 10000.0f; + settings.white_point_x = in.read_u32() / DIVISION_FACTOR; + settings.white_point_y = in.read_u32() / DIVISION_FACTOR; + settings.red_x = in.read_u32() / DIVISION_FACTOR; + settings.red_y = in.read_u32() / DIVISION_FACTOR; + settings.green_x = in.read_u32() / DIVISION_FACTOR; + settings.green_y = in.read_u32() / DIVISION_FACTOR; + settings.blue_x = in.read_u32() / DIVISION_FACTOR; + settings.blue_y = in.read_u32() / DIVISION_FACTOR; +} + +void Decoder::decode_chunk_image_gamma(uint32_t chunk_size) +{ + assert_chunk_size(4, chunk_size, "Image gamma"); + + const double DIVISION_FACTOR = 10000.0f; + settings.gamma = in.read_u32() / DIVISION_FACTOR; +} + +void Decoder::decode_chunk_icc_profile(uint32_t chunk_size) +{ + char character; + while ((character = in.read_u8()) != '\0') + settings.icc_profile_name += character; + + settings.icc_compression_method = (CompressionMethod)in.read_u8(); + + // TODO: Support icc_compressed_profile + in.skip(chunk_size - settings.icc_profile_name.size() - 2); +} + +void Decoder::decode_chunk_significant_bit(uint32_t chunk_size) +{ + if (settings.color_type == ColorType::GREYSCALE && chunk_size == 1) + { + settings.significant_greyscale_bits = in.read_u8(); + return; + } + + if ((settings.color_type == ColorType::TRUE_COLOR || settings.color_type == ColorType::INDEXED_COLOR) && chunk_size == 3) + { + settings.significant_red_bits = in.read_u8(); + settings.significant_green_bits = in.read_u8(); + settings.significant_blue_bits = in.read_u8(); + return; + } + + if (settings.color_type == ColorType::GREYSCALE_WITH_ALPHA && chunk_size == 2) + { + settings.significant_greyscale_bits = in.read_u8(); + settings.significant_alpha_bits = in.read_u8(); + return; + } + + if (settings.color_type == ColorType::TRUE_COLOR_WITH_ALPHA && chunk_size == 4) + { + settings.significant_red_bits = in.read_u8(); + settings.significant_green_bits = in.read_u8(); + settings.significant_blue_bits = in.read_u8(); + settings.significant_alpha_bits = in.read_u8(); + return; + } + + throw std::runtime_error { "Wrong significant bits chunk_size, or unsupported color_type." }; +} + +void Decoder::decode_chunk_standard_rgb(uint32_t chunk_size) +{ + assert_chunk_size(1, chunk_size, "Standard RGB"); + + settings.rendering_intent = (RenderingIntent)in.read_u8(); + + settings.gamma = 45455; + + settings.white_point_x = 31270; + settings.white_point_y = 32900; + settings.red_x = 64000; + settings.red_y = 33000; + settings.green_x = 30000; + settings.green_y = 60000; + settings.blue_x = 15000; + settings.blue_y = 6000; +} + +void Decoder::decode_chunk_background_color(uint32_t chunk_size) +{ + if ((settings.color_type == ColorType::GREYSCALE || settings.color_type == ColorType::GREYSCALE_WITH_ALPHA) && chunk_size == 2) + { + settings.background_greyscale = in.read_u16(); + return; + } + + if ((settings.color_type == ColorType::TRUE_COLOR || settings.color_type == ColorType::TRUE_COLOR_WITH_ALPHA) && chunk_size == 6) + { + settings.background_red = in.read_u16(); + settings.background_green = in.read_u16(); + settings.background_blue = in.read_u16(); + return; + } + + if (settings.color_type == ColorType::INDEXED_COLOR && chunk_size == 2) + { + settings.background_palette_index = in.read_u8(); + return; + } + + throw std::runtime_error { "Wrong background color chunk_size, or unsupported color_type." }; +} + +void Decoder::decode_chunk_image_histogram(uint32_t chunk_size) +{ + // TODO: Support histogram + in.skip(chunk_size); +} + +void Decoder::decode_chunk_transparency(uint32_t chunk_size) +{ + assert_chunk_size(1, chunk_size, "Transparency"); +} + +void Decoder::decode_chunk_physical_pixel_dimensions(uint32_t chunk_size) +{ + assert_chunk_size(9, chunk_size, "Physical pixel dimensions"); + + settings.pixels_per_unit_x = in.read_u32(); + settings.pixels_per_unit_y = in.read_u32(); + settings.unit_specifier = (UnitSpecifier)in.read_u8(); +} + +void Decoder::decode_chunk_suggested_palette(uint32_t chunk_size) +{ + // TODO: Support suggested palette + in.skip(chunk_size); +} + +void Decoder::decode_chunk_image_last_modification_time(uint32_t chunk_size) +{ + assert_chunk_size(7, chunk_size, "Last modification date"); + + settings.last_modification_date = { + in.read_u16(), + in.read_u8(), + in.read_u8(), + in.read_u8(), + in.read_u8(), + in.read_u8(), + }; +} + +void Decoder::decode_chunk_textual_data(uint32_t chunk_size) +{ + std::string keyword = decode_string(); + + uint8_t remaining_bytes = chunk_size - keyword.size() - 1; + std::string text = decode_string_of_size(remaining_bytes); + + settings.textual_data.push_back({ + keyword, + text, + }); +} + +void Decoder::decode_chunk_compressed_textual_data(uint32_t chunk_size) +{ + std::string keyword = decode_string(); + + uint8_t compression_method = in.read_u8(); + + uint8_t remaining_bytes = chunk_size - keyword.size() - 1; + std::string text = decode_string_of_size(remaining_bytes); + + settings.compressed_textual_data.push_back({ + keyword, + text, + compression_method, + }); +} + +void Decoder::decode_chunk_international_textual_data(uint32_t chunk_size) +{ + std::string keyword = decode_string(); + + uint8_t compression_flag = in.read_u8(); + uint8_t compression_method = in.read_u8(); + + std::string language_tag = decode_string(); + std::string translated_keyword = decode_string(); + + uint8_t remaining_bytes = chunk_size - keyword.size() - language_tag.size() - translated_keyword.size() - 5; + + std::string text = decode_string_of_size(remaining_bytes); + + settings.international_textual_data.push_back({ + keyword, + text, + compression_flag, + compression_method, + language_tag, + }); +} + +std::string Decoder::decode_string() const +{ + std::string value; + char character; + while ((character = in.read_u8()) != '\0') + value += character; + + return value; +} + +std::string Decoder::decode_string_of_size(size_t size) const +{ + std::string value; + for (size_t i = 0; i < size; ++i) + value += in.read_u8(); + + return value; +} + +void Decoder::assert_chunk_size(uint32_t expected, uint32_t current, const std::string &chunk_name) +{ + if (expected == current) return; + throw std::runtime_error { chunk_name + " chunk size must be " + std::to_string(expected) + ". Current = " + std::to_string(current) }; +} + +void Decoder::uncompress_image_data() +{ + uncompressed_image_data = Compression::DeflateCompressor(compressed_image_data).uncompress(); +} + +void Decoder::unfilter_image_data() +{ + if (settings.filter_method != FilterMethod::ADAPTIVE) + throw std::runtime_error { "The adaptive filter method is the only one supported." }; + + /* + for (auto it = image_data.begin(); it != image_data.end(); ++it) + { + uint8_t filter_type = 0; + switch (filter_type) + { + case 0: + break; + case 1: + break; + case 2: + break; + case 3: + break; + case 4: + break; + default: + throw std::runtime_error { "The filter_type " + std::to_string(filter_type) + "doesn't exist." }; + } + } + */ +} + +void Decoder::unserialize_scanlines() +{ +} + +} diff --git a/src/png/png_decoder.hpp b/src/png/png_decoder.hpp new file mode 100644 index 0000000..b6aaec3 --- /dev/null +++ b/src/png/png_decoder.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +#include "decoder.hpp" +#include "png_common.hpp" +#include "png_settings.hpp" +#include "raw_image.hpp" +#include "stream.hpp" + +namespace PNG { + +class Decoder final : public BaseDecoder +{ +private: + Settings settings; + + BufferStream compressed_image_data {}; + std::vector uncompressed_image_data {}; + +public: + Decoder(InputStream &in, RawImage &image) : + BaseDecoder(in, image) {} + + static bool can_decode(InputStream &in); + + void decode() override; + + Settings get_settings() const { return settings; } + +private: + void decode_chunk(); + + void decode_chunk_header(uint32_t chunk_size); + void decode_chunk_palette(uint32_t chunk_size); + void decode_chunk_image_data(uint32_t chunk_size); + void decode_chunk_image_trailer(uint32_t chunk_size); + + void decode_chunk_primary_chroma_and_white_points(uint32_t chunk_size); + void decode_chunk_image_gamma(uint32_t chunk_size); + void decode_chunk_icc_profile(uint32_t chunk_size); + void decode_chunk_significant_bit(uint32_t chunk_size); + void decode_chunk_standard_rgb(uint32_t chunk_size); + void decode_chunk_background_color(uint32_t chunk_size); + void decode_chunk_image_histogram(uint32_t chunk_size); + void decode_chunk_transparency(uint32_t chunk_size); + void decode_chunk_physical_pixel_dimensions(uint32_t chunk_size); + void decode_chunk_suggested_palette(uint32_t chunk_size); + void decode_chunk_image_last_modification_time(uint32_t chunk_size); + void decode_chunk_textual_data(uint32_t chunk_size); + void decode_chunk_compressed_textual_data(uint32_t chunk_size); + void decode_chunk_international_textual_data(uint32_t chunk_size); + + void read_scanline(); + void read_scanline_byte(); + + std::string decode_string() const; + std::string decode_string_of_size(size_t size) const; + + void assert_chunk_size(uint32_t expected, uint32_t current, const std::string &chunk_name); + + void uncompress_image_data(); + void unfilter_image_data(); + void unserialize_scanlines(); +}; + +} diff --git a/src/png/png_encoder.cpp b/src/png/png_encoder.cpp new file mode 100644 index 0000000..208f8fb --- /dev/null +++ b/src/png/png_encoder.cpp @@ -0,0 +1,11 @@ +#include "png_encoder.hpp" + +#include "pixel.hpp" + +namespace PNG { + +void Encoder::encode() +{ +} + +} diff --git a/src/png/png_encoder.hpp b/src/png/png_encoder.hpp new file mode 100644 index 0000000..233f128 --- /dev/null +++ b/src/png/png_encoder.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "png_common.hpp" +#include "png_settings.hpp" +#include "raw_image.hpp" +#include "stream.hpp" + +namespace PNG { + +class Encoder final : public BaseEncoder +{ +private: + Settings settings; + +public: + Encoder(OutputStream &out, RawImage &image) : + BaseEncoder(out, image) {} + + void encode() override; + + void set_settings(const Settings &settings) { this->settings = settings; } +}; + +} diff --git a/src/png/png_format.hpp b/src/png/png_format.hpp new file mode 100644 index 0000000..ee9eed8 --- /dev/null +++ b/src/png/png_format.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "format.hpp" + +static const Format PNG_FORMAT{ + "png", + "Portable Network Graphics", + {"png"}, +}; diff --git a/src/png/png_settings.hpp b/src/png/png_settings.hpp new file mode 100644 index 0000000..0af20db --- /dev/null +++ b/src/png/png_settings.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#include "pixel.hpp" +#include "png_common.hpp" + +namespace PNG { + +struct Settings +{ + // Header + BitDepth bit_depth; + ColorType color_type; + CompressionMethod compression_method; + FilterMethod filter_method; + InterlaceMethod interlace_method; + + // Palette + std::vector palette; + + // Primary chroma and white points + double white_point_x, white_point_y; + double red_x, red_y; + double green_x, green_y; + double blue_x, blue_y; + + // Gamma + double gamma; + + // ICC Profile + std::string icc_profile_name; + CompressionMethod icc_compression_method; + + // Significant bits + uint8_t significant_greyscale_bits; + uint8_t significant_red_bits; + uint8_t significant_green_bits; + uint8_t significant_blue_bits; + uint8_t significant_alpha_bits; + + // Standard RGB + RenderingIntent rendering_intent; + + // Text data + std::vector textual_data {}; + std::vector compressed_textual_data {}; + std::vector international_textual_data {}; + + // Background + uint16_t background_greyscale; + uint16_t background_red; + uint16_t background_green; + uint16_t background_blue; + uint8_t background_palette_index; + + // Physical pixel dimensions + uint32_t pixels_per_unit_x, pixels_per_unit_y; + UnitSpecifier unit_specifier; + + // Last modification date + Date last_modification_date; +}; + +} diff --git a/test_images/image_1_ultrasmall.png b/test_images/image_1_ultrasmall.png new file mode 100644 index 0000000..a389622 Binary files /dev/null and b/test_images/image_1_ultrasmall.png differ diff --git a/test_images/out_image_1.png b/test_images/out_image_1.png new file mode 100644 index 0000000..e69de29 diff --git a/tests/common_tests.cpp b/tests/common_tests.cpp index 277f2ad..40ea9f5 100644 --- a/tests/common_tests.cpp +++ b/tests/common_tests.cpp @@ -1,7 +1,13 @@ #include #include +#include +#include +#include +#include +#include #include +#include "deflate_compressor.hpp" #include "gtest/gtest.h" #include "stream.hpp" @@ -48,7 +54,7 @@ static void test_read(InputStream& in) EXPECT_EQ(in.read_u1(), 0); EXPECT_EQ(in.peek_u1(), 1); EXPECT_EQ(in.read_u1(), 1); - EXPECT_EQ(in.read_bits(4), 0b0011); + EXPECT_EQ(in.read_bits(4), 0b0011); EXPECT_EQ(in.peek_u16(), 30000); EXPECT_EQ(in.read_u16(), 30000); EXPECT_EQ(in.peek_i16(), -25000); @@ -94,3 +100,62 @@ TEST(CommonTest, FileStream) remove("temporary_stream_test"); } + +TEST(CommonTest, HuffmanTree) +{ + std::vector> nodes = { + std::make_shared(2, 'A'), + std::make_shared(1, 'B'), + std::make_shared(3, 'C'), + std::make_shared(3, 'D'), + }; + + Compression::HuffmanTree tree { nodes }; + BufferStream stream {}; + stream.write_u16_le(0b0000001101110101); + ASSERT_EQ(tree.get_value(stream), 'A'); + ASSERT_EQ(tree.get_value(stream), 'A'); + ASSERT_EQ(tree.get_value(stream), 'D'); + ASSERT_EQ(tree.get_value(stream), 'B'); + ASSERT_EQ(tree.get_value(stream), 'C'); +} + +TEST(CommonTest, DeflateCompressorLevel0) +{ + BufferStream stream {}; + + stream.write_u32(0x7801010e); + stream.write_u32(0x00f1ff53); + stream.write_u32(0x6f6d6520); + stream.write_u32(0x74657874); + stream.write_u32(0x20686572); + stream.write_u32(0x6526e205); + stream.write_u8(0x3e); + + Compression::DeflateCompressor compressor { stream }; + std::vector uncompressed_data = compressor.uncompress(); + std::string s { uncompressed_data.begin(), uncompressed_data.end() }; + EXPECT_EQ(s, "Some text here"); +} + +TEST(CommonTest, DeflateCompressorLevel1) +{ + BufferStream stream {}; + + stream.write_u32(0x78da0bce); + stream.write_u32(0xcf4d5528); + stream.write_u32(0x49ad2851); + stream.write_u32(0xc8482d02); + stream.write_u32(0xb232124b); + stream.write_u32(0x14328b15); + stream.write_u32(0x8a520b52); + stream.write_u32(0x134b5253); + stream.write_u32(0x74148af1); + stream.write_u32(0x2bd00300); + stream.write_u32(0x09bc1783); + + Compression::DeflateCompressor compressor { stream }; + std::vector uncompressed_data = compressor.uncompress(); + std::string s { uncompressed_data.begin(), uncompressed_data.end() }; + EXPECT_EQ(s, "Some text here that is repeated, some text here that is repeated."); +} diff --git a/tests/conversion_tests.cpp b/tests/conversion_tests.cpp index 1c3612c..07d9f97 100644 --- a/tests/conversion_tests.cpp +++ b/tests/conversion_tests.cpp @@ -2,6 +2,8 @@ #include "bmp_encoder.hpp" #include "conversion_common.hpp" #include "gtest/gtest.h" +#include "png_decoder.hpp" +#include "png_encoder.hpp" #include "qoi_decoder.hpp" #include "qoi_encoder.hpp" @@ -17,3 +19,5 @@ TEST_ENCODE_AND_DECODE(BMP, "image_1_small_16bpp_x1r5g5b5.bmp", 16bppX1R5G5B5); TEST_ENCODE_AND_DECODE(BMP, "image_1_small_24bpp_r8g8b8.bmp", 24bppR8G8B8); TEST_ENCODE_AND_DECODE(BMP, "image_1_small_32bpp_a8r8g8b8.bmp", 32bppA8R8G8B8); TEST_ENCODE_AND_DECODE(BMP, "image_1_small_32bpp_x8r8g8b8.bmp", 32bppX8R8G8B8); + +TEST_ENCODE_AND_DECODE(PNG, "image_1.png", Default); diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index 4b0b3ee..6b83dae 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -1,5 +1,6 @@ set(HEADERS image_widget_bmp.hpp + image_widget_png.hpp image_widget_qoi.hpp image_widget.hpp app.hpp @@ -7,6 +8,7 @@ set(HEADERS set(SOURCES image_widget_bmp.cpp + image_widget_png.cpp image_widget_qoi.cpp image_widget.cpp main.cpp diff --git a/viewer/app.cpp b/viewer/app.cpp index a4b722a..f5215ab 100644 --- a/viewer/app.cpp +++ b/viewer/app.cpp @@ -14,8 +14,11 @@ #include "bmp_format.hpp" #include "image_widget.hpp" #include "image_widget_bmp.hpp" +#include "image_widget_png.hpp" #include "image_widget_qoi.hpp" #include "imgui.h" +#include "png_decoder.hpp" +#include "png_format.hpp" #include "qoi_decoder.hpp" #include "qoi_format.hpp" #include "raw_image.hpp" @@ -166,6 +169,8 @@ void App::open_image_widget(const std::string& path) image_widgets.push_back(std::move(QOIImageWidget { in, path })); else if (BMP::Decoder::can_decode(in)) image_widgets.push_back(std::move(BMPImageWidget { in, path })); + else if (PNG::Decoder::can_decode(in)) + image_widgets.push_back(std::move(PNGImageWidget { in, path })); else throw std::runtime_error { "Couldn't decode this image." }; } diff --git a/viewer/image_widget_png.cpp b/viewer/image_widget_png.cpp new file mode 100644 index 0000000..12fb005 --- /dev/null +++ b/viewer/image_widget_png.cpp @@ -0,0 +1,13 @@ +#include "image_widget_png.hpp" + +#include "png_decoder.hpp" + +namespace Viewer { + +PNGImageWidget::PNGImageWidget(InputStream& in, const std::string& filename) : + ImageWidget { filename } +{ + PNG::Decoder { in, raw_image }.decode(); +} + +} diff --git a/viewer/image_widget_png.hpp b/viewer/image_widget_png.hpp new file mode 100644 index 0000000..c93097c --- /dev/null +++ b/viewer/image_widget_png.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "image_widget.hpp" +#include "stream.hpp" + +namespace Viewer { + +class PNGImageWidget : public ImageWidget +{ +public: + PNGImageWidget(InputStream& in, const std::string& filename); +}; + +}