diff --git a/.gitmodules b/.gitmodules index 16ffeeb..aba77f8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "external/FastLZ"] path = external/FastLZ url = https://github.com/ariya/FastLZ.git -[submodule "external/murmurhash"] - path = external/murmurhash - url = https://github.com/jwerle/murmurhash.c.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f7ad32c..08aeb57 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,15 +27,14 @@ FetchContent_MakeAvailable(argparse) # yes... im using glob... dont judge me.... file(GLOB_RECURSE PROJECT_SOURCES CONFIGURE_DEPENDS "src/*.cpp") file(GLOB EXTERNAL_SOURCES CONFIGURE_DEPENDS - "external/murmurhash/murmurhash.c" "external/FastLZ/fastlz.c") set(PROJECT_INCLUDE "src" ${STB_INCLUDE_DIRS} ${LIQ_INCLUDE_DIRS} + ${COLORM_INCLUDE_DIRS} "external/ChernoTimer" - "external/murmurhash" "external/FastLZ") set(PROJECT_LIBRARY diff --git a/external/murmurhash b/external/murmurhash deleted file mode 160000 index 10ba9c2..0000000 --- a/external/murmurhash +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 10ba9c25abbcf8952b5f4abecf9bf4fc148e8e65 diff --git a/readme.txt b/readme.txt index cc566f4..464cd57 100644 --- a/readme.txt +++ b/readme.txt @@ -11,22 +11,22 @@ the idea is to simply load image data without worrying about image metadata. the metadata contain basic stuff like width, height, format, etc... color palette are embedded in the file. -a palette hash is provided. this can be used to comparing -whether another image uses the same palette usage: sillyimage -i -o [options] options: - -i, --input input files [required] [may be repeated] - -o, --out output directory [required] - -h, --help shows help message and exits - -v, --version prints version information and exits - -f, --format texture format { rgb256, rgb16, indexed4, indexed16, indexed256, indexed32a3, indexed8a5 } [default: "rgb16"] + -i, --input input files [required] + -o, --out output directory [required] + -h, --help shows help message and exits + -v, --version prints version information and exits + -f, --format texture format { rgb256, rgb16, indexed4, indexed16, indexed256, indexed32a8, indexed8a32 } [default: "rgb16"] + -pf, --palette-format palette format { rgb16, rgb256 } [default: "rgb16"] + -be, --big-endian enable big endian mode [default: false] binary format: [string] sillyimg (0x676D69796C6C6973 in hex or 7452728928599042419 in decimal (uint64)) - [uint8] version (current version is 13) - [uint8] format + [int8] version (current version is 14) + [int8] format 0 - RGB256 1 - RGB16 2 - INDEXED4 @@ -35,13 +35,16 @@ binary format: 5 - INDEXED32A3 6 - INDEXED8A5 7 - PALETTE16 - [uint16] width - [uint16] height - [uint16] palette count - [uint32] palette hash - [uint32] compress size - [uint32] original size - [palette buffer] + [int8] big endian mode + [int8] palette format + 1 - rgb16 (2 bytes per palette) + 2 - rgb256 (4 bytes per palette) + [int16] width + [int16] height + [int16] palette count + [int32] original size + [int32] compress size + [palette buffer] (palette count * palette format size) [image buffer] TODO: @@ -55,8 +58,9 @@ TODO: dependencies: argparse (https://github.com/p-ranav/argparse) stb_image (https://github.com/nothings/stb) - murmurhash.c (https://github.com/jwerle/murmurhash.c) + libimagequant (https://github.com/ImageOptim/libimagequant) Timer.h from cherno (https://gist.github.com/TheCherno/b2c71c9291a4a1a29c889e76173c8d14) + FastLZ (https://github.com/ariya/FastLZ) license: GPL v3 \ No newline at end of file diff --git a/src/header.hpp b/src/header.hpp index 10f0687..54663a9 100644 --- a/src/header.hpp +++ b/src/header.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 sillysagiri + * Copyright (C) 2025 sillysagiri * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * @@ -16,23 +16,16 @@ #include #include -// wporkaround surpress clangd pragma pack warning -// https://github.com/clangd/clangd/issues/1167 -static_assert(true); - -#pragma pack(push, 1) struct Metadata { uint64_t header = 0x676D69796C6C6973; - uint8_t version = 13; - uint8_t format; - uint16_t width; - uint16_t height; - uint16_t palette_count; - uint32_t palette_hash; - uint32_t original_size; - uint32_t compress_size; + int8_t version = 14; + int8_t format; + int16_t width; + int16_t height; + int16_t palette_count; + int32_t original_size; + int32_t compress_size; }; -#pragma pack(pop) enum Format { @@ -71,12 +64,37 @@ typedef std::vector ImageMapped; typedef std::vector Palette; struct Options { - std::vector path_input; + std::string path_input; std::string path_output; std::string format; + std::string palette_format; + // std::string compression; + bool enable_be = false; }; -void Verify(); -void Convert(); +void WriteOutput(const Options &opt, const Metadata &meta, const Palette &palette, void *image_buffer); +ImageMapped Quantize(const Image &image, Palette &palette_output, int num_colors, bool dither, uint8_t format); -std::vector Quantize(const std::vector& images, Palette &palette_output, int num_colors, bool dither, uint8_t format); \ No newline at end of file +static inline uint16_t to_be_int16(int16_t value) { + uint16_t uval = static_cast(value); + return (uval >> 8) | (uval << 8); +} + +static inline uint32_t to_be_int32(int32_t value) { + uint32_t uval = static_cast(value); + return ((uval >> 24) & 0x000000FF) | + ((uval >> 8) & 0x0000FF00) | + ((uval << 8) & 0x00FF0000) | + ((uval << 24) & 0xFF000000); +} + +static inline uint16_t to_be_int16(uint16_t value) { + return (value >> 8) | (value << 8); +} + +static inline uint32_t to_be_int32(uint32_t value) { + return ((value >> 24) & 0x000000FF) | + ((value >> 8) & 0x0000FF00) | + ((value << 8) & 0x00FF0000) | + ((value << 24) & 0xFF000000); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b2a9f56..30e8a4c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 sillysagiri + * Copyright (C) 2025 sillysagiri * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * @@ -8,16 +8,9 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#include #include -#include -#include #include -#include -#include -#include -#include - +#include #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION @@ -25,22 +18,22 @@ #include "header.hpp" #include "Timer.h" #include "fastlz.h" -#include "murmurhash.h" namespace fs = std::filesystem; int main(int argc, char **argv) { srand(time(0)); - argparse::ArgumentParser program("sillyimage", "1.3"); + argparse::ArgumentParser program("sillyimage", "0.14"); Options opt; // Register argument { program.add_argument("-i", "--input") .help("input files") - .append() + .default_value("") .required() + .nargs(1) .store_into(opt.path_input); program.add_argument("-o", "--out") @@ -54,6 +47,26 @@ int main(int argc, char **argv) .choices("rgb256", "rgb16", "indexed4", "indexed16", "indexed256", "indexed32a8", "indexed8a32") .nargs(1) .store_into(opt.format); + + program.add_argument("-pf", "--palette-format") + .help("palette format { rgb16, rgb256 }") + .default_value("rgb16") + .choices("rgb16", "rgb256") + .nargs(1) + .store_into(opt.palette_format); + + // program.add_argument("-c", "--compression") + // .help("compression { none, fastlz }") + // .default_value("fastlz") + // .choices("none", "fastlz") + // .nargs(1) + // .store_into(opt.compression); + + program.add_argument("-be", "--big-endian") + .help("enable big endian mode") + .default_value(false) + .nargs(1) + .store_into(opt.enable_be); } // Parse argument @@ -63,9 +76,8 @@ int main(int argc, char **argv) program.parse_args(argc, argv); // input list - for (auto &i : opt.path_input) - if (i.empty() || !fs::exists(i)) - throw std::runtime_error(std::format("file doesnt exsist: \"{}\"", i)); + if (opt.path_input.empty() || !fs::exists(opt.path_input)) + throw std::runtime_error(std::format("file doesnt exsist: \"{}\"", opt.path_input)); // output directory if (opt.path_output.empty() || !fs::is_directory(opt.path_output)) @@ -84,59 +96,60 @@ int main(int argc, char **argv) if (opt.format == "rgb256") { - for(auto &input : opt.path_input) + Image img; + img.Load(opt.path_input); + + uint32_t img255_size = img.width*img.height; + uint32_t img255[img255_size]; + for (int i=0; i(img.data[i*4+3]) << 24) | // A + (static_cast(img.data[i*4+0]) << 16) | // R + (static_cast(img.data[i*4+1]) << 8) | // G + static_cast(img.data[i*4+2]); // B - Metadata meta; - meta.format = Format::Format_RGB_256; - meta.width = img.width; - meta.height = img.height; - meta.palette_count = 0; - meta.palette_hash = 0; - meta.original_size = img.data.size(); - - uint8_t compress[uint64_t(meta.original_size*1.5)]; - meta.compress_size = fastlz_compress_level(1, img.data.data(), meta.original_size, compress); - - auto output = fs::path(opt.path_output) / std::format("{}.sillyimg", fs::path(input).stem().string()); - std::ofstream out(output, std::ios::binary); - out.write(reinterpret_cast(&meta), sizeof(meta)); - out.write(reinterpret_cast(compress), meta.compress_size); - out.close(); + if (opt.enable_be) + img255[i] = to_be_int32(img255[i]); } + + Metadata meta; + meta.format = Format::Format_RGB_256; + meta.width = img.width; + meta.height = img.height; + meta.palette_count = 0; + meta.original_size = img.data.size(); + + uint8_t compress[uint64_t(meta.original_size*1.5)]; + meta.compress_size = fastlz_compress_level(1, img.data.data(), meta.original_size, compress); + + Palette palette(0); + + WriteOutput(opt, meta, palette, compress); } if (opt.format == "rgb16") { - for(auto &input : opt.path_input) - { - Image img; - img.Load(input); - std::vector img16(img.width*img.height); + Image img; + img.Load(opt.path_input); + std::vector img16(img.width*img.height); - // convert into RGB16 color - for (int i=0; i(&meta), sizeof(meta)); - out.write(reinterpret_cast(compress), meta.compress_size); - out.close(); - } + Palette palette(0); + + WriteOutput(opt, meta, palette, compress); } if (opt.format == "indexed4" || @@ -175,41 +188,22 @@ int main(int argc, char **argv) } Palette palette; - std::vector palette16; - std::vector images(opt.path_input.size()); + Image image; + image.Load(opt.path_input); + + ImageMapped remap = Quantize(image, palette, numcol, false, format); - for (int i=0; i(palette16.data()), sizeof(uint16_t)*palette16.size(), 0); - - for (int i=0; i(&meta), sizeof(meta)); - out.write(reinterpret_cast(palette16.data()), sizeof(uint16_t)*palette.size()); - out.write(reinterpret_cast(compress), meta.compress_size); - out.close(); - } + WriteOutput(opt, meta, palette, compress); } } catch(const std::exception &e) @@ -236,8 +230,85 @@ void Image::Load(const std::filesystem::path &path) stbi_uc *ptr_img = stbi_load(path.c_str(), &width, &height, &comp, 4); if (ptr_img == nullptr) throw std::runtime_error(stbi_failure_reason()); - int size = width*height*comp; - data.assign(ptr_img, ptr_img+size); + int size = width*height*4; + data.reserve(size); + for (int i = 0; i < size; ++i) + data.push_back(ptr_img[i]); stbi_image_free(ptr_img); +} + +void WriteOutput(const Options &opt, const Metadata &meta, const Palette &palette, void *image_buffer) +{ + std::filesystem::path input_file(opt.path_input); + std::filesystem::path output_dir(opt.path_output); + std::string basename = input_file.stem(); + + std::string output_file = output_dir / std::format("{}.sillyimg", basename); + std::ofstream out(output_file, std::ios::binary); + + out.write(reinterpret_cast(&meta.header), sizeof(uint64_t)); + out.write(reinterpret_cast(&meta.version), sizeof(int8_t)); + out.write(reinterpret_cast(&meta.format), sizeof(int8_t)); + + int8_t isBE = opt.enable_be; + int8_t palette_format; + if (opt.palette_format == "rgb16") palette_format = 1; + if (opt.palette_format == "rgb256") palette_format = 2; + + out.write(reinterpret_cast(&isBE), sizeof(int8_t)); + out.write(reinterpret_cast(&palette_format), sizeof(int8_t)); + + if (!opt.enable_be) + { + out.write(reinterpret_cast(&meta.width), sizeof(int16_t)); + out.write(reinterpret_cast(&meta.height), sizeof(int16_t)); + out.write(reinterpret_cast(&meta.palette_count), sizeof(int16_t)); + out.write(reinterpret_cast(&meta.original_size), sizeof(int32_t)); + out.write(reinterpret_cast(&meta.compress_size), sizeof(int32_t)); + } + else + { + uint16_t be_width = to_be_int16(meta.width); + uint16_t be_height = to_be_int16(meta.height); + uint16_t be_palette_count = to_be_int16(meta.palette_count); + uint32_t be_original_size = to_be_int32(meta.original_size); + uint32_t be_compress_size = to_be_int32(meta.compress_size); + + out.write(reinterpret_cast(&be_width), sizeof(uint16_t)); + out.write(reinterpret_cast(&be_height), sizeof(uint16_t)); + out.write(reinterpret_cast(&be_palette_count), sizeof(uint16_t)); + out.write(reinterpret_cast(&be_original_size), sizeof(uint32_t)); + out.write(reinterpret_cast(&be_compress_size), sizeof(uint32_t)); + } + + if (!palette.empty()) + { + if (palette_format == 1) + { + uint16_t pal16[meta.palette_count]; + for (int i=0; i(pal16), sizeof(uint16_t)*meta.palette_count); + } + else if (palette_format == 2) + { + uint32_t pal256[meta.palette_count]; + for (int i=0; i(palette[i].a) << 24) | // A + (static_cast(palette[i].r) << 16) | // R + (static_cast(palette[i].g) << 8) | // G + static_cast(palette[i].b); // B + + if (opt.enable_be) pal256[i] = to_be_int32(pal256[i]); + } + + out.write(reinterpret_cast(pal256), sizeof(uint32_t)*meta.palette_count); + } + } + + out.write(reinterpret_cast(image_buffer), meta.compress_size); + out.close(); } \ No newline at end of file diff --git a/src/quantizer.cpp b/src/quantizer.cpp index f0f4abf..e83cac0 100644 --- a/src/quantizer.cpp +++ b/src/quantizer.cpp @@ -1,31 +1,41 @@ +/* + * Copyright (C) 2025 sillysagiri + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + +#include +#include #include #include #include "header.hpp" -std::vector Quantize(const std::vector& images, Palette &palette_output, int num_colors, bool dither, uint8_t format) +ImageMapped Quantize(const Image &image, Palette &palette_output, int num_colors, bool dither, uint8_t format) { liq_attr *attr = liq_attr_create(); - liq_histogram *hist = liq_histogram_create(attr); liq_set_max_colors(attr, num_colors); + liq_set_speed(attr, 1); + liq_set_quality(attr, 0, 100); - std::vector liq_images; - liq_images.reserve(images.size()); - - for (const Image &img : images) - { - liq_image *liq_img = liq_image_create_rgba(attr, img.data.data(), img.width, img.height, 0); - liq_histogram_add_image(hist, attr, liq_img); - liq_images.push_back(liq_img); - } + liq_image *liq_img = liq_image_create_rgba(attr, image.data.data(), image.width, image.height, 0); liq_result *result; - if (LIQ_OK != liq_histogram_quantize(hist, attr, &result)) + if (LIQ_OK != liq_image_quantize(liq_img, attr, &result)) throw std::runtime_error("failed to quantize histogram"); if (dither) liq_set_dithering_level(result, 1.0f); else liq_set_dithering_level(result, 0.0f); + ImageMapped output(image.width*image.height); + + if (LIQ_OK != liq_write_remapped_image(result, liq_img, output.data(), output.size())) + throw std::runtime_error("failed to remap image"); + const liq_palette *pal = liq_get_palette(result); palette_output.resize(pal->count); for (int i=0; icount; i++) @@ -36,38 +46,25 @@ std::vector Quantize(const std::vector& images, Palette &pal palette_output[i].a = pal->entries[i].a; } - std::vector output(images.size()); - - for (int i=0; i packed(output.size()); + ImageMapped packed; + packed.reserve((output.size()+3) / 4); - for (int i=0; i Quantize(const std::vector& images, Palette &pal else if (format == Format_INDEXED_16) { - std::vector packed(output.size()); + ImageMapped packed; + packed.reserve((output.size()+3) / 4); - for (int i=0; i Quantize(const std::vector& images, Palette &pal else if (format == Format_INDEXED_8A32) { - std::vector packed(output.size()); + ImageMapped packed; + packed.reserve((output.size()+3) / 4); - for (int i=0; i> 3; - current_byte |= (output[i][offset] & 0b111) | (alpha << 3); - packed[i].push_back(current_byte); - } + uint8_t alpha = image.data[offset*4+3] >> 3; + current_byte |= (output[offset] & 0b111) | (alpha << 3); + packed.push_back(current_byte); } return packed; @@ -117,19 +106,15 @@ std::vector Quantize(const std::vector& images, Palette &pal else if (format == Format_INDEXED_32A8) { - std::vector packed(output.size()); + ImageMapped packed(output.size()); + packed.reserve((output.size()+3) / 4); - for (int i=0; i> 5; - current_byte |= (output[i][offset] & 0b11111) | (alpha << 5); - packed[i].push_back(current_byte); - } + uint8_t alpha = image.data[offset*4+3] >> 5; + current_byte |= (output[offset] & 0b11111) | (alpha << 5); + packed.push_back(current_byte); } return packed;