v0.14 intial release

This commit is contained in:
sillysagiri 2025-06-16 00:19:27 +07:00
parent 77b0fb85bd
commit 9bd6c5be71
7 changed files with 275 additions and 202 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -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

1
external/murmurhash vendored

@ -1 +0,0 @@
Subproject commit 10ba9c25abbcf8952b5f4abecf9bf4fc148e8e65

View File

@ -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 <input image> -o <output dir> [options]
options:
-i, --input input files [required] [may be repeated]
-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, indexed32a3, indexed8a5 } [default: "rgb16"]
-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

View File

@ -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 <string>
#include <vector>
// 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<uint8_t> ImageMapped;
typedef std::vector<Color> Palette;
struct Options {
std::vector<std::string> 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<ImageMapped> Quantize(const std::vector<Image>& images, Palette &palette_output, int num_colors, bool dither, uint8_t format);
static inline uint16_t to_be_int16(int16_t value) {
uint16_t uval = static_cast<uint16_t>(value);
return (uval >> 8) | (uval << 8);
}
static inline uint32_t to_be_int32(int32_t value) {
uint32_t uval = static_cast<uint32_t>(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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <format>
#include <iostream>
#include <vector>
#include <stdint.h>
#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<std::string>("")
.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<std::string>("rgb16")
.choices("rgb16", "rgb256")
.nargs(1)
.store_into(opt.palette_format);
// program.add_argument("-c", "--compression")
// .help("compression { none, fastlz }")
// .default_value<std::string>("fastlz")
// .choices("none", "fastlz")
// .nargs(1)
// .store_into(opt.compression);
program.add_argument("-be", "--big-endian")
.help("enable big endian mode")
.default_value<bool>(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))
@ -83,37 +95,42 @@ int main(int argc, char **argv)
try {
if (opt.format == "rgb256")
{
for(auto &input : opt.path_input)
{
Image img;
img.Load(input);
img.Load(opt.path_input);
uint32_t img255_size = img.width*img.height;
uint32_t img255[img255_size];
for (int i=0; i<img255_size; i++)
{
img255[i] = (static_cast<uint32_t>(img.data[i*4+3]) << 24) | // A
(static_cast<uint32_t>(img.data[i*4+0]) << 16) | // R
(static_cast<uint32_t>(img.data[i*4+1]) << 8) | // G
static_cast<uint32_t>(img.data[i*4+2]); // B
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.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<const char*>(&meta), sizeof(meta));
out.write(reinterpret_cast<const char*>(compress), meta.compress_size);
out.close();
}
Palette palette(0);
WriteOutput(opt, meta, palette, compress);
}
if (opt.format == "rgb16")
{
for(auto &input : opt.path_input)
{
Image img;
img.Load(input);
img.Load(opt.path_input);
std::vector<uint16_t> img16(img.width*img.height);
// convert into RGB16 color
@ -125,18 +142,14 @@ int main(int argc, char **argv)
meta.width = img.width;
meta.height = img.height;
meta.palette_count = 0;
meta.palette_hash = 0;
meta.original_size = img16.size()*sizeof(uint16_t);
uint8_t compress[uint64_t(meta.original_size*1.5)];
meta.compress_size = fastlz_compress_level(2, img16.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<const char*>(&meta), sizeof(meta));
out.write(reinterpret_cast<const char*>(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<uint16_t> palette16;
std::vector<Image> images(opt.path_input.size());
Image image;
image.Load(opt.path_input);
for (int i=0; i<images.size(); i++)
images[i].Load(opt.path_input[i]);
ImageMapped remap = Quantize(image, palette, numcol, false, format);
auto remap = Quantize(images, palette, numcol, false, format);
palette16.resize(palette.size());
for (int i=1; i<palette.size(); i++)
palette16[i] = RGB255_RGB16(palette[i].r, palette[i].g, palette[i].b, palette[i].a);
uint32_t hash = murmurhash(reinterpret_cast<const char*>(palette16.data()), sizeof(uint16_t)*palette16.size(), 0);
for (int i=0; i<images.size(); i++)
{
Metadata meta;
meta.format = format;
meta.width = images[i].width;
meta.height = images[i].height;
meta.palette_count = palette16.size();
meta.palette_hash = hash;
meta.original_size = remap[i].size();
meta.width = image.width;
meta.height = image.height;
meta.palette_count = palette.size();
meta.original_size = remap.size();
uint8_t compress[uint64_t(meta.original_size*1.5)];
meta.compress_size = fastlz_compress_level(2, remap[i].data(), meta.original_size, compress);
meta.compress_size = fastlz_compress_level(2, remap.data(), meta.original_size, compress);
auto output = fs::path(opt.path_output) / std::format("{}.sillyimg", fs::path(opt.path_input[i]).stem().string());
std::ofstream out(output, std::ios::binary);
out.write(reinterpret_cast<const char*>(&meta), sizeof(meta));
out.write(reinterpret_cast<const char*>(palette16.data()), sizeof(uint16_t)*palette.size());
out.write(reinterpret_cast<const char*>(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<const char*>(&meta.header), sizeof(uint64_t));
out.write(reinterpret_cast<const char*>(&meta.version), sizeof(int8_t));
out.write(reinterpret_cast<const char*>(&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<const char*>(&isBE), sizeof(int8_t));
out.write(reinterpret_cast<const char*>(&palette_format), sizeof(int8_t));
if (!opt.enable_be)
{
out.write(reinterpret_cast<const char*>(&meta.width), sizeof(int16_t));
out.write(reinterpret_cast<const char*>(&meta.height), sizeof(int16_t));
out.write(reinterpret_cast<const char*>(&meta.palette_count), sizeof(int16_t));
out.write(reinterpret_cast<const char*>(&meta.original_size), sizeof(int32_t));
out.write(reinterpret_cast<const char*>(&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<const char*>(&be_width), sizeof(uint16_t));
out.write(reinterpret_cast<const char*>(&be_height), sizeof(uint16_t));
out.write(reinterpret_cast<const char*>(&be_palette_count), sizeof(uint16_t));
out.write(reinterpret_cast<const char*>(&be_original_size), sizeof(uint32_t));
out.write(reinterpret_cast<const char*>(&be_compress_size), sizeof(uint32_t));
}
if (!palette.empty())
{
if (palette_format == 1)
{
uint16_t pal16[meta.palette_count];
for (int i=0; i<meta.palette_count; i++)
pal16[i] = RGB255_RGB16(palette[i].r, palette[i].g, palette[i].b, palette[i].a);
out.write(reinterpret_cast<const char*>(pal16), sizeof(uint16_t)*meta.palette_count);
}
else if (palette_format == 2)
{
uint32_t pal256[meta.palette_count];
for (int i=0; i<meta.palette_count; i++)
{
pal256[i] = (static_cast<uint32_t>(palette[i].a) << 24) | // A
(static_cast<uint32_t>(palette[i].r) << 16) | // R
(static_cast<uint32_t>(palette[i].g) << 8) | // G
static_cast<uint32_t>(palette[i].b); // B
if (opt.enable_be) pal256[i] = to_be_int32(pal256[i]);
}
out.write(reinterpret_cast<const char*>(pal256), sizeof(uint32_t)*meta.palette_count);
}
}
out.write(reinterpret_cast<const char*>(image_buffer), meta.compress_size);
out.close();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <cstdio>
#include <libimagequant.h>
#include <stdexcept>
#include "header.hpp"
std::vector<ImageMapped> Quantize(const std::vector<Image>& 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_image*> 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; i<pal->count; i++)
@ -36,38 +46,25 @@ std::vector<ImageMapped> Quantize(const std::vector<Image>& images, Palette &pal
palette_output[i].a = pal->entries[i].a;
}
std::vector<ImageMapped> output(images.size());
for (int i=0; i<images.size(); i++)
{
output[i].resize(images[i].width*images[i].height);
if (LIQ_OK != liq_write_remapped_image(result, liq_images[i], output[i].data(), output[i].size()))
throw std::runtime_error("failed to remap image");
}
for (auto &i : liq_images) liq_image_destroy(i);
liq_histogram_destroy(hist);
liq_image_destroy(liq_img);
liq_attr_destroy(attr);
liq_result_destroy(result);
// ---
if (format == Format_INDEXED_4)
{
std::vector<ImageMapped> packed(output.size());
ImageMapped packed;
packed.reserve((output.size()+3) / 4);
for (int i=0; i<output.size(); i++)
{
packed[i].reserve((output[i].size()+3) / 4);
for (int chunk_start=0; chunk_start<output[i].size(); chunk_start+=4) {
for (int chunk_start=0; chunk_start<output.size(); chunk_start+=4) {
uint8_t current_byte = 0;
for (int offset=0; offset<4 && (chunk_start+offset)<output[i].size(); ++offset) {
current_byte |= (output[i][chunk_start + offset] & 0b11) << (offset * 2);
for (int offset=0; offset<4 && (chunk_start+offset)<output.size(); ++offset) {
current_byte |= (output[chunk_start + offset] & 0b11) << (offset * 2);
}
packed[i].push_back(current_byte);
}
packed.push_back(current_byte);
}
return packed;
@ -75,21 +72,17 @@ std::vector<ImageMapped> Quantize(const std::vector<Image>& images, Palette &pal
else if (format == Format_INDEXED_16)
{
std::vector<ImageMapped> packed(output.size());
ImageMapped packed;
packed.reserve((output.size()+3) / 4);
for (int i=0; i<output.size(); i++)
{
packed[i].reserve((output[i].size()+3) / 4);
for (int offset=0; offset<output[i].size(); offset+=2) {
for (int offset=0; offset<output.size(); offset+=2) {
uint8_t current_byte = 0;
current_byte |= (output[i][offset] & 0b1111);
if (i+1 <output[i].size())
current_byte |= (output[i][offset + 1] & 0b1111) << 4;
current_byte |= (output[offset] & 0b1111);
if (offset+1 <output.size())
current_byte |= (output[offset+1] & 0b1111) << 4;
packed[i].push_back(current_byte);
}
packed.push_back(current_byte);
}
return packed;
@ -97,19 +90,15 @@ std::vector<ImageMapped> Quantize(const std::vector<Image>& images, Palette &pal
else if (format == Format_INDEXED_8A32)
{
std::vector<ImageMapped> packed(output.size());
ImageMapped packed;
packed.reserve((output.size()+3) / 4);
for (int i=0; i<output.size(); i++)
{
packed[i].reserve((output[i].size()+3) / 4);
for (int offset=0; offset<output[i].size(); offset++) {
for (int offset=0; offset<output.size(); offset++) {
uint8_t current_byte = 0;
uint8_t alpha = images[i].data[offset*4+3] >> 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<ImageMapped> Quantize(const std::vector<Image>& images, Palette &pal
else if (format == Format_INDEXED_32A8)
{
std::vector<ImageMapped> packed(output.size());
ImageMapped packed(output.size());
packed.reserve((output.size()+3) / 4);
for (int i=0; i<output.size(); i++)
{
packed[i].reserve((output[i].size()+3) / 4);
for (int offset=0; offset<output[i].size(); offset++) {
for (int offset=0; offset<output.size(); offset++) {
uint8_t current_byte = 0;
uint8_t alpha = images[i].data[offset*4+3] >> 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;