emote2ss

Animated webp to spritesheets converting tool
git clone git://bsandro.tech/emote2ss
Log | Files | Refs | README | LICENSE

commit 08c9c53b4c6b366ed3c6bd0ff8d39f92e9dae1d3
parent 540f40220ae4f484b1e5f6d5a77f514b86b40eb1
Author: bsandro <[email protected]>
Date:   Sun, 29 Sep 2024 23:28:48 +0300

Basic split into different logic files

Diffstat:
MTODO | 12++++++++++--
Mgui/animation.c | 5+++--
Mgui/animation.h | 4++++
Agui/fileops.c | 48++++++++++++++++++++++++++++++++++++++++++++++++
Agui/fileops.h | 12++++++++++++
Mgui/main.c | 426+++----------------------------------------------------------------------------
Agui/spritesheet.c | 36++++++++++++++++++++++++++++++++++++
Agui/spritesheet.h | 13+++++++++++++
Agui/ui.c | 316+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/ui.h | 28++++++++++++++++++++++++++++
10 files changed, 486 insertions(+), 414 deletions(-)

diff --git a/TODO b/TODO @@ -1,3 +1,4 @@ +- Memory leak with file open/save dialog :( - Add 'debug' and 'release' builds into makefile - Wrap errors into sane messages and not just asserts - Support drag-n-drop for GUI version @@ -6,5 +7,12 @@ - i18n - Status bar with service/info messages - Fix preview display - pixels with alpha channel are borked -- Better file open/save dialog -- Split into files/headers +- File open/save dialog: display current path on open +- Better main window layout +- Split into logical files/headers +- Display/update resulting spritesheet dimensions in pixels +- Unify path and filename fields (more canonical way) +- `-> copy the full path+name into the unified field +- M$ Windows build +- Use system open/save dialog on Windows? + diff --git a/gui/animation.c b/gui/animation.c @@ -1,6 +1,7 @@ #define _GNU_SOURCE #include "animation.h" +#include "fileops.h" #include <libgen.h> #include <stdlib.h> #include <stdio.h> @@ -54,8 +55,8 @@ int WebpRead(const char *filename, Animation *anim) { WebPData webp_data; WebPDataInit(&webp_data); - if (ReadFile(filename, &webp_data.bytes, &webp_data.size) == -1) { - fprintf(stderr, "ReadFile error\n"); + if (FileRead(filename, &webp_data.bytes, &webp_data.size) == -1) { + fprintf(stderr, "FileRead() error\n"); return -1; } diff --git a/gui/animation.h b/gui/animation.h @@ -1,3 +1,6 @@ +/* webp animations utility stuff */ +#pragma once + #include <stdbool.h> #include <stdint.h> @@ -22,3 +25,4 @@ typedef struct { Animation * ImageLoad(const char *path); void ImageUnload(Animation **img); int WebpRead(const char *filename, Animation *anim); +void AnimationCreate(Animation *img, uint32_t frame_count); diff --git a/gui/fileops.c b/gui/fileops.c @@ -0,0 +1,48 @@ +#include "webp/decode.h" +#include "webp/encode.h" +#include "webp/demux.h" +#include "fileops.h" +#include "spritesheet.h" + +int FileRead(const char *filename, const uint8_t **data, size_t *size) { + assert(data != NULL); + assert(size != NULL); + + *data = NULL; + *size = 0; + FILE *infile = fopen(filename, "rb"); + assert(infile != NULL); + fseek(infile, 0, SEEK_END); + size_t fsize = ftell(infile); + // printf("%s: %zu bytes\n", filename, fsize); + fseek(infile, 0, SEEK_SET); + + uint8_t *fdata = malloc(fsize+1); + assert(fdata != NULL); + fdata[fsize] = '\0'; + int ok = (fread(fdata, fsize, 1, infile)==1); + fclose(infile); + + if (!ok) { + fprintf(stderr, "cannot read file %s (%d)\n", filename, ok); + free(fdata); + return -1; + } + *data = fdata; + *size = fsize; + return 0; +} + +void WebpWrite(const char *path, Animation *img, int cols) { + Spritesheet ss = SpritesheetGen(img, cols); + uint8_t *out; + FILE *fp = fopen(path, "wb"); + assert(fp!=NULL); + size_t encoded = WebPEncodeLosslessRGBA(ss.data, ss.width, ss.height, ss.stride, &out); + // printf("size: %zu, encoded: %zu\n", img->width*img->height*sizeof(uint32_t), encoded); + assert(encoded!=0); + size_t written = fwrite(out, sizeof(uint8_t), encoded, fp); + assert(written==encoded); + WebPFree(out); + free(ss.data); +} diff --git a/gui/fileops.h b/gui/fileops.h @@ -0,0 +1,12 @@ +/* file i/o operations */ +#pragma once + +#include <stdint.h> +#include <stddef.h> +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include "animation.h" + +int FileRead(const char *filename, const uint8_t **data, size_t *size); +void WebpWrite(const char *path, Animation *img, int cols); diff --git a/gui/main.c b/gui/main.c @@ -10,20 +10,20 @@ #define UI_IMPLEMENTATION #include <stdio.h> +//#include <stdbool.h> +//#include <stdlib.h> +//#include <libgen.h> +//#include <string.h> +//#include <strings.h> +//#include <assert.h> +//#include <dirent.h> +#include <sys/prctl.h> #include "webp/decode.h" #include "webp/encode.h" #include "webp/demux.h" -#include <stdbool.h> -#include <stdlib.h> -#include <libgen.h> -#include <string.h> -#include <strings.h> -#include <assert.h> -#include <dirent.h> -#include <sys/prctl.h> -#include "luigi.h" - +//#include "luigi.h" #include "animation.h" +#include "ui.h" #ifdef __OpenBSD__ #include <sys/syslimits.h> @@ -33,17 +33,9 @@ #include <linux/limits.h> #endif -#ifdef _WIN32 -#define DIR_SEPARATOR '\\' -#else -#define DIR_SEPARATOR '/' -#endif - #define WIN_WIDTH 750 #define WIN_HEIGHT 400 -#define UI_COLOR_FROM_RGBA(r, g, b, a) (((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | ((uint32_t) (b) << 0) | ((uint32_t) (a) << 24)) - static void print_webp_version() { int dec_ver = WebPGetDecoderVersion(); int demux_ver = WebPGetDemuxVersion(); @@ -52,375 +44,8 @@ static void print_webp_version() { printf("webp demuxer version: %d.%d.%d\n", (demux_ver>>16)&0xff, (demux_ver>>8)&0xff, demux_ver&0xff); } -typedef struct { - uint8_t *data; - size_t len; - int width, height, stride; -} Spritesheet; - // let it be global variable for now -static Animation *img = NULL; -static int cols = 1; -static volatile UIWindow *modal_win = NULL; -static UIImageDisplay *img_disp = NULL; -static UILabel *lbl_header = NULL; - -//@todo headers! -//Animation * ImageLoad(const char *); -//void ImageUnload(Animation **); -void PreviewUpdate(Animation *, UIImageDisplay *); - -// @todo collision with ms windows function -int ReadFile(const char *filename, const uint8_t **data, size_t *size) { - assert(data != NULL); - assert(size != NULL); - - *data = NULL; - *size = 0; - FILE *infile = fopen(filename, "rb"); - assert(infile != NULL); - fseek(infile, 0, SEEK_END); - size_t fsize = ftell(infile); - // printf("%s: %zu bytes\n", filename, fsize); - fseek(infile, 0, SEEK_SET); - - uint8_t *fdata = malloc(fsize+1); - assert(fdata != NULL); - fdata[fsize] = '\0'; - int ok = (fread(fdata, fsize, 1, infile)==1); - fclose(infile); - - if (!ok) { - fprintf(stderr, "cannot read file %s (%d)\n", filename, ok); - free(fdata); - return -1; - } - *data = fdata; - *size = fsize; - return 0; -} - -Spritesheet GenSpritesheet(Animation *img, int cols) { - int rows = (int)img->frame_count / cols; - if ((int)img->frame_count % cols > 0) { - ++rows; - } - size_t frame_size = img->width * img->height * sizeof(uint32_t); - size_t line_size = img->width * sizeof(uint32_t); - size_t full_line = line_size * cols; - uint8_t *merged = calloc(rows * cols, frame_size); - assert(merged!=NULL); - uint8_t *merged_orig = merged; - for (int row = 0; row < rows; ++row) { - for (int y = 0; y < img->height; ++y) { - for (int col = 0; col < cols; ++col) { - uint32_t offset = row*cols+col; - if (offset < img->frame_count) { - memcpy(merged, img->frames[offset].rgba+y*line_size, line_size); - } - merged += line_size; - } - } - } - int stride = full_line; - Spritesheet ss = {0}; - ss.data = merged_orig; - ss.stride = stride; - ss.width = cols * img->width; - ss.height = rows * img->height; - ss.len = ss.width * ss.height * sizeof(uint32_t); - - return ss; -} - -void WebpWrite(const char *path, Animation *img) { - Spritesheet ss = GenSpritesheet(img, cols); - uint8_t *out; - FILE *fp = fopen(path, "wb"); - assert(fp!=NULL); - size_t encoded = WebPEncodeLosslessRGBA(ss.data, ss.width, ss.height, ss.stride, &out); - // printf("size: %zu, encoded: %zu\n", img->width*img->height*sizeof(uint32_t), encoded); - assert(encoded!=0); - size_t written = fwrite(out, sizeof(uint8_t), encoded, fp); - assert(written==encoded); - WebPFree(out); - free(ss.data); -} - -int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_DESTROY) { - // printf("modal bye\n"); - assert(element==modal_win); - modal_win = NULL; - } - return 0; -} - -int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_CLICKED) { - // get values of path and filename inputs - UITextbox *path_input = (UITextbox *)element->parent->children; - UITextbox *filename_input = (UITextbox *)path_input->e.next; - // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input); - // that printf might work incorrectly because path_input->string might not contain valid C string with \0 at the end - // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, path_input->bytes, filename_input->string, filename_input->bytes); - int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0' - // printf("path_len: %d\n", path_len); - char out_name[path_len]; - out_name[path_input->bytes] = DIR_SEPARATOR; - out_name[path_len-1] = '\0'; - memcpy(out_name, path_input->string, path_input->bytes); - memcpy(out_name+path_input->bytes+1, filename_input->string, filename_input->bytes); - // printf("strlen(out_name): %d\n", strlen(out_name)); - // printf("out_name: %s\n", out_name); - WebpWrite(out_name, img); - // printf("save dialog window close\n"); - assert(element->window==modal_win); - UIElementDestroy(element->window); - } - return 0; - } - -int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_CLICKED) { - // printf("open dialog window close\n"); - UITextbox *path_input = (UITextbox *)element->parent->children; - UITextbox *filename_input = (UITextbox *)path_input->e.next; - // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input); - // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, strlen(path_input->string), filename_input->string, strlen(filename_input->string)); - - UIElementDestroy(element->window); - - int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0' - char filepath[path_len]; - filepath[path_input->bytes] = DIR_SEPARATOR; - filepath[path_len-1] = '\0'; - memcpy(filepath, path_input->string, path_input->bytes); - memcpy(filepath+path_input->bytes+1, filename_input->string, filename_input->bytes); - - if (img != NULL) { - ImageUnload(&img); - assert(img==NULL); - } - img = ImageLoad(filepath); - assert(img!=NULL); - PreviewUpdate(img, img_disp); - UILabelSetContent(lbl_header, img->path, -1); - } - return 0; -} - -static int FilelistFilter(const struct dirent *f) { - if (strncmp(f->d_name, ".", 255)==0) return 0; - if (f->d_type==DT_DIR) return 1; - //@todo not very efficient - size_t len = strlen(f->d_name); - // checking for the ".webp" at the very end - if (len<6) return 0; - return strncmp(f->d_name+len-5, ".webp", 255)==0; -} - -static int FilelistCompare(const struct dirent **a, const struct dirent **b) { - if ((*a)->d_type==DT_DIR && (*b)->d_type==DT_DIR) { - if (strncmp((*a)->d_name, "..", 255)==0) return -1; - else if (strncmp((*b)->d_name, "..", 255)==0) return 1; - else return alphasort(a, b); - } - if ((*a)->d_type==DT_DIR) return -1; - if ((*b)->d_type==DT_DIR) return 1; - return alphasort(a, b); -} - -int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) { - static int selected = -1; - static struct dirent **filelist = NULL; - char *dir_name = element->cp; - UITable *table = (UITable *)element; - if (filelist==NULL) { - table->itemCount = scandir(dir_name, &filelist, FilelistFilter, FilelistCompare); - // printf("populated dir %s with %d items\n", dir_name, table->itemCount); - } - if (msg==UI_MSG_TABLE_GET_ITEM) { - UITableGetItem *m = (UITableGetItem *)dp; - m->isSelected = selected==m->index; - bool is_dir = filelist[m->index]->d_type==DT_DIR; - //printf("render %s (%d)\n", filelist[m->index]->d_name, m->bufferBytes); - int ret = snprintf(m->buffer, m->bufferBytes, is_dir?"[%s]":"%s", filelist[m->index]->d_name); - //printf("%s - %d (%d)\n", filelist[m->index]->d_name, ret, m->bufferBytes); - return ret; - } else if (msg==UI_MSG_CLICKED) { - int hit = UITableHitTest(table, element->window->cursorX, element->window->cursorY); - if (hit!=-1 && selected==hit) { - if (filelist[hit]->d_type==DT_DIR) { - char newpath[PATH_MAX]; - bzero(newpath, PATH_MAX); - strcpy(newpath, element->cp); - strcat(newpath, "/"); - strcat(newpath, filelist[hit]->d_name); - free(element->cp); - element->cp = realpath(newpath, NULL); - //free(filelist); - filelist = NULL; - selected = -1; - //@todo duplicated code - UIPanel *panel_out = (UIPanel *)element->parent; - UILabel *label = (UILabel *)panel_out->e.children; - UIPanel *panel_top = (UIPanel *)label->e.next; - UITextbox *path_input = (UITextbox *)panel_top->e.children; - UITextboxClear(path_input, false); - UITextboxReplace(path_input, (char *)element->cp, -1, false); - UIElementRepaint(path_input, NULL); - UITableEnsureVisible(table, -1); - } - } else { - selected = hit; - if (!UITableEnsureVisible(table, selected)) { - UIElementRepaint(element, NULL); - } - //@todo remove copypasta - if (hit!=-1 && filelist[hit]->d_type!=DT_DIR) { - UIPanel *panel_out = (UIPanel *)element->parent; - UILabel *label = (UILabel *)panel_out->e.children; - UIPanel *panel_top = (UIPanel *)label->e.next; - UITextbox *path_input = (UITextbox *)panel_top->e.children; - UITextbox *file_input = (UITextbox *)path_input->e.next; - UITextboxClear(path_input, false); - UITextboxClear(file_input, false); - UITextboxReplace(path_input, (char *)element->cp, -1, false); - UITextboxReplace(file_input, filelist[hit]->d_name, -1, false); - UIElementRepaint(path_input, NULL); - UIElementRepaint(file_input, NULL); - } - } - } else if (msg==UI_MSG_DESTROY) { - printf("destroy\n"); - // free(filelist); - filelist = NULL; - selected = -1; - } else if (msg==UI_MSG_UPDATE) { - UITableResizeColumns(table); - } - return 0; -} - -int ButtonCloseEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_CLICKED) { - UIElementDestroy(element->window); - } - return 0; -} - -typedef int (*CallbackFn)(struct UIElement *element, UIMessage message, int di, void *dp); - -void ShowModalWindow(UIWindow *parent, const char *def_dir, const char *def_file, CallbackFn cb) { - if (modal_win == NULL) { - // printf("create modal window\n"); - modal_win = UIWindowCreate(parent, NULL, def_file?"Save File":"Open File", WIN_WIDTH, WIN_HEIGHT); - modal_win->e.messageUser = WinModalEvent; - UIPanel *panel_out = UIPanelCreate(&modal_win->e, UI_PANEL_GRAY|UI_PANEL_EXPAND); - UILabel *lbl_title = UILabelCreate(&panel_out->e, 0, def_file?"Save File":"Open File", -1); - UIPanel *panel_top = UIPanelCreate(&panel_out->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL); - UITextbox *path_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_DISABLED|UI_ELEMENT_H_FILL); - if (def_dir != NULL) { - UITextboxReplace(path_input, def_dir, -1, false); - } - - UITextbox *filename_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_TAB_STOP|UI_ELEMENT_H_FILL); - - printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input); - if (def_file != NULL) { - UITextboxReplace(filename_input, def_file, -1, false); - } - - UIButton *btn_save = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, def_file?"Save":"Open", -1); - btn_save->e.messageUser = cb; - - UIButton *btn_cancel = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, "Cancel", -1); - btn_cancel->e.messageUser = ButtonCloseEvent; - - UITable *table = UITableCreate(&panel_out->e, UI_ELEMENT_V_FILL, "Directory"); - table->itemCount = 1; // at least '..' element - // @todo this way setting the initial directory is kinda trashcan - char *init_path = (char *)malloc(2*sizeof(char)); - init_path[0] = '.'; - init_path[1] = '\0'; - table->e.cp = (void *)init_path; - table->e.messageUser = TableEvent; - UITableResizeColumns(table); - } else { - UIElementFocus(modal_win); - } -} - -int ButtonSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_CLICKED) { - // printf("save button clicked\n"); - char fname[strlen(img->filename)+7]; - int n = snprintf(fname, NAME_MAX, "atlas_%s", img->filename); - assert(n>0); - ShowModalWindow(element->window, img->dirname, fname, ButtonDialogSaveEvent); - } - return 0; -} - -int ButtonOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_CLICKED) { - // printf("open button clicked\n"); - ShowModalWindow(element->window, getenv("HOME"), NULL, ButtonDialogOpenEvent); - } - return 0; -} - -void PreviewUpdate(Animation *img, UIImageDisplay *img_disp) { - // gen new spritesheet and refresh the preview - Spritesheet ss = GenSpritesheet(img, cols); - //printf("spritesheet width: %d, height: %d, stride: %d, len: %zu\n", ss.width, ss.height, ss.stride, ss.len); - - uint32_t *frame0 = calloc(ss.width*ss.height, sizeof(uint32_t)); - assert(frame0!=NULL); - - for (uint32_t i=0; i<ss.width*ss.height; ++i) { - frame0[i] = UI_COLOR_FROM_RGBA(ss.data[i*4+0], ss.data[i*4+1], ss.data[i*4+2], ss.data[i*4+3]); - } - UIImageDisplaySetContent(img_disp, frame0, ss.width, ss.height, ss.stride); - UIElementRefresh(img_disp->e.parent); - UIElementRefresh(&img_disp->e); - - free(frame0); - free(ss.data); - - UILabel *label = (UILabel *)img_disp->e.cp; - char label_text[256] = {0}; - snprintf(&label_text, 255, "Width: %3d tiles", cols); - UILabelSetContent(label, label_text, -1); - UIElementRefresh(label->e.parent); -} - -int SliderEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_VALUE_CHANGED) { - if (img == NULL) return 1; - - float slider_pos = ((UISlider *)element)->position; - float step = 1.0f / (float)img->frame_count; - int new_cols = (int)(slider_pos / step); - if (new_cols > 0 && cols != new_cols) { - //printf("new_cols: %d\n", new_cols); - cols = new_cols; - PreviewUpdate(img, element->cp); - } - } - - return 0; -} - -int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp) { - if (msg == UI_MSG_DESTROY) { - // printf("bye\n"); - free(img); - exit(0); - } - return 0; -} +//static Animation *img = NULL; #ifdef UI_LINUX int main(int argc, const char **argv) { @@ -429,39 +54,20 @@ int WinMain(HINSTANCE instance, HINSTANCE previousInstance, LPSTR commandLine, i #endif // just some silly stuff, always wanted to try that - prctl(PR_SET_NAME, "webp-anim-2-spritesheet"); - // printf("argv[0]: %s\n", argv[0]); + prctl(PR_SET_NAME, "webp-anim-to-spritesheet"); atexit(print_webp_version); UIInitialise(); - UIWindow *win = UIWindowCreate(0, 0, "emote2ss", WIN_WIDTH, WIN_HEIGHT); - win->e.messageUser = WinMainEvent; - UIPanel *panelv = UIPanelCreate(&win->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING); - lbl_header = UILabelCreate(&panelv->e, 0, "filename", -1); - - UIPanel *panelh = UIPanelCreate(&panelv->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL); - UILabel *label = UILabelCreate(&panelh->e, 0, "webp to spritesheet converter", -1); - UISlider *slider = UISliderCreate(&panelh->e, 0); - UIButton *btnopen = UIButtonCreate(&panelh->e, 0, "Open", -1); - btnopen->e.messageUser = ButtonOpenEvent; - UIButton *btnsave = UIButtonCreate(&panelh->e, 0, "Save", -1); - btnsave->e.messageUser = ButtonSaveEvent; - btnsave->e.cp = win; - UIButton *btn_exit = UIButtonCreate(&panelh->e, 0, "Close", -1); - btn_exit->e.messageUser = ButtonCloseEvent; - - img_disp = UIImageDisplayCreate(&panelv->e, UI_ELEMENT_V_FILL|UI_ELEMENT_H_FILL|UI_IMAGE_DISPLAY_INTERACTIVE|_UI_IMAGE_DISPLAY_ZOOM_FIT, NULL, 0, 0, 0); - img_disp->e.cp = label; - slider->e.messageUser = SliderEvent; - slider->e.cp = img_disp; + UIWindow *win = MainWindowCreate("emote2ss", WIN_WIDTH, WIN_HEIGHT); - if (argc > 1) { + // do I actually need this? + /*if (argc > 1) { img = ImageLoad(argv[1]); assert(img!=NULL); PreviewUpdate(img, img_disp); - } + }*/ return UIMessageLoop(); } diff --git a/gui/spritesheet.c b/gui/spritesheet.c @@ -0,0 +1,36 @@ +#include <assert.h> +#include "spritesheet.h" + +Spritesheet SpritesheetGen(Animation *img, int cols) { + int rows = (int)img->frame_count / cols; + if ((int)img->frame_count % cols > 0) { + ++rows; + } + size_t frame_size = img->width * img->height * sizeof(uint32_t); + size_t line_size = img->width * sizeof(uint32_t); + size_t full_line = line_size * cols; + uint8_t *merged = calloc(rows * cols, frame_size); + assert(merged!=NULL); + uint8_t *merged_orig = merged; + for (int row = 0; row < rows; ++row) { + for (int y = 0; y < img->height; ++y) { + for (int col = 0; col < cols; ++col) { + uint32_t offset = row*cols+col; + if (offset < img->frame_count) { + memcpy(merged, img->frames[offset].rgba+y*line_size, line_size); + } + merged += line_size; + } + } + } + int stride = full_line; + Spritesheet ss = {0}; + ss.data = merged_orig; + ss.stride = stride; + ss.width = cols * img->width; + ss.height = rows * img->height; + ss.len = ss.width * ss.height * sizeof(uint32_t); + + return ss; +} + diff --git a/gui/spritesheet.h b/gui/spritesheet.h @@ -0,0 +1,13 @@ +#pragma once + +#include <stdint.h> +#include <stddef.h> +#include "animation.h" + +typedef struct { + uint8_t *data; + size_t len; + int width, height, stride; +} Spritesheet; + +Spritesheet SpritesheetGen(Animation *img, int cols); diff --git a/gui/ui.c b/gui/ui.c @@ -0,0 +1,316 @@ +#define _DEFAULT_SOURCE + +#include <dirent.h> +#include "fileops.h" +#include "animation.h" +#include "ui.h" +#include "spritesheet.h" + +static int cols = 1; +static volatile UIWindow *modal_win = NULL; +static UIImageDisplay *img_disp = NULL; +static UILabel *lbl_header = NULL; +static Animation *img = NULL; + +static int FilelistFilter(const struct dirent *f) { + if (strncmp(f->d_name, ".", 255)==0) return 0; + if (f->d_type==DT_DIR) return 1; + //@todo not very efficient + size_t len = strlen(f->d_name); + // checking for the ".webp" at the very end + if (len<6) return 0; + return strncmp(f->d_name+len-5, ".webp", 255)==0; +} + +static int FilelistCompare(const struct dirent **a, const struct dirent **b) { + if ((*a)->d_type==DT_DIR && (*b)->d_type==DT_DIR) { + if (strncmp((*a)->d_name, "..", 255)==0) return -1; + else if (strncmp((*b)->d_name, "..", 255)==0) return 1; + else return alphasort(a, b); + } + if ((*a)->d_type==DT_DIR) return -1; + if ((*b)->d_type==DT_DIR) return 1; + return alphasort(a, b); +} + +UIWindow * MainWindowCreate(const char *wname, int w, int h) { + UIWindow *win = UIWindowCreate(0, 0, wname, w, h); + win->e.messageUser = WinMainEvent; + UIPanel *panelv = UIPanelCreate(&win->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING); + lbl_header = UILabelCreate(&panelv->e, 0, "filename", -1); + + UIPanel *panelh = UIPanelCreate(&panelv->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL); + UILabel *label = UILabelCreate(&panelh->e, 0, "webp to spritesheet converter", -1); + UISlider *slider = UISliderCreate(&panelh->e, 0); + UIButton *btnopen = UIButtonCreate(&panelh->e, 0, "Open", -1); + btnopen->e.messageUser = ButtonOpenEvent; + UIButton *btnsave = UIButtonCreate(&panelh->e, 0, "Save", -1); + btnsave->e.messageUser = ButtonSaveEvent; + btnsave->e.cp = win; + UIButton *btn_exit = UIButtonCreate(&panelh->e, 0, "Close", -1); + btn_exit->e.messageUser = ButtonCloseEvent; + + img_disp = UIImageDisplayCreate(&panelv->e, UI_ELEMENT_V_FILL|UI_ELEMENT_H_FILL|UI_IMAGE_DISPLAY_INTERACTIVE|_UI_IMAGE_DISPLAY_ZOOM_FIT, NULL, 0, 0, 0); + img_disp->e.cp = label; + slider->e.messageUser = SliderEvent; + slider->e.cp = img_disp; + return win; +} + +int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_DESTROY) { + // printf("modal bye\n"); + assert(element==modal_win); + modal_win = NULL; + } + return 0; +} + +int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_CLICKED) { + // get values of path and filename inputs + UITextbox *path_input = (UITextbox *)element->parent->children; + UITextbox *filename_input = (UITextbox *)path_input->e.next; + // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input); + // that printf might work incorrectly because path_input->string might not contain valid C string with \0 at the end + // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, path_input->bytes, filename_input->string, filename_input->bytes); + int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0' + // printf("path_len: %d\n", path_len); + char out_name[path_len]; + out_name[path_input->bytes] = DIR_SEPARATOR; + out_name[path_len-1] = '\0'; + memcpy(out_name, path_input->string, path_input->bytes); + memcpy(out_name+path_input->bytes+1, filename_input->string, filename_input->bytes); + // printf("strlen(out_name): %d\n", strlen(out_name)); + // printf("out_name: %s\n", out_name); + WebpWrite(out_name, img, cols); + // printf("save dialog window close\n"); + assert(element->window==modal_win); + UIElementDestroy(element->window); + } + return 0; + } + +int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_CLICKED) { + // printf("open dialog window close\n"); + UITextbox *path_input = (UITextbox *)element->parent->children; + UITextbox *filename_input = (UITextbox *)path_input->e.next; + // printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input); + // printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, strlen(path_input->string), filename_input->string, strlen(filename_input->string)); + + UIElementDestroy(element->window); + + int path_len = path_input->bytes + filename_input->bytes + 2; // DIR_SEPARATOR and '\0' + char filepath[path_len]; + filepath[path_input->bytes] = DIR_SEPARATOR; + filepath[path_len-1] = '\0'; + memcpy(filepath, path_input->string, path_input->bytes); + memcpy(filepath+path_input->bytes+1, filename_input->string, filename_input->bytes); + + if (img != NULL) { + ImageUnload(&img); + assert(img==NULL); + } + img = ImageLoad(filepath); + assert(img!=NULL); + PreviewUpdate(img, img_disp); + UILabelSetContent(lbl_header, img->path, -1); + } + return 0; +} + +int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) { + static int selected = -1; + static struct dirent **filelist = NULL; + char *dir_name = element->cp; + UITable *table = (UITable *)element; + if (filelist==NULL) { + table->itemCount = scandir(dir_name, &filelist, FilelistFilter, FilelistCompare); + // printf("populated dir %s with %d items\n", dir_name, table->itemCount); + } + if (msg==UI_MSG_TABLE_GET_ITEM) { + UITableGetItem *m = (UITableGetItem *)dp; + m->isSelected = selected==m->index; + bool is_dir = filelist[m->index]->d_type==DT_DIR; + //printf("render %s (%d)\n", filelist[m->index]->d_name, m->bufferBytes); + int ret = snprintf(m->buffer, m->bufferBytes, is_dir?"[%s]":"%s", filelist[m->index]->d_name); + //printf("%s - %d (%d)\n", filelist[m->index]->d_name, ret, m->bufferBytes); + return ret; + } else if (msg==UI_MSG_CLICKED) { + int hit = UITableHitTest(table, element->window->cursorX, element->window->cursorY); + if (hit!=-1 && selected==hit) { + if (filelist[hit]->d_type==DT_DIR) { + char newpath[PATH_MAX]; + bzero(newpath, PATH_MAX); + strcpy(newpath, element->cp); + strcat(newpath, "/"); + strcat(newpath, filelist[hit]->d_name); + free(element->cp); + element->cp = realpath(newpath, NULL); + //free(filelist); + filelist = NULL; + selected = -1; + //@todo duplicated code + UIPanel *panel_out = (UIPanel *)element->parent; + UILabel *label = (UILabel *)panel_out->e.children; + UIPanel *panel_top = (UIPanel *)label->e.next; + UITextbox *path_input = (UITextbox *)panel_top->e.children; + UITextboxClear(path_input, false); + UITextboxReplace(path_input, (char *)element->cp, -1, false); + UIElementRepaint(path_input, NULL); + UITableEnsureVisible(table, -1); + } + } else { + selected = hit; + if (!UITableEnsureVisible(table, selected)) { + UIElementRepaint(element, NULL); + } + //@todo remove copypasta + if (hit!=-1 && filelist[hit]->d_type!=DT_DIR) { + UIPanel *panel_out = (UIPanel *)element->parent; + UILabel *label = (UILabel *)panel_out->e.children; + UIPanel *panel_top = (UIPanel *)label->e.next; + UITextbox *path_input = (UITextbox *)panel_top->e.children; + UITextbox *file_input = (UITextbox *)path_input->e.next; + UITextboxClear(path_input, false); + UITextboxClear(file_input, false); + UITextboxReplace(path_input, (char *)element->cp, -1, false); + UITextboxReplace(file_input, filelist[hit]->d_name, -1, false); + UIElementRepaint(path_input, NULL); + UIElementRepaint(file_input, NULL); + } + } + } else if (msg==UI_MSG_DESTROY) { + free(filelist); + filelist = NULL; + selected = -1; + UIElement *el = table->e.children; + printf("child %p\n", el); + while (el!=NULL) { + printf("child %p\n", el); + } + } else if (msg==UI_MSG_UPDATE) { + UITableResizeColumns(table); + } + return 0; +} + +int ButtonCloseEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_CLICKED) { + UIElementDestroy(element->window); + } + return 0; +} + +void ShowModalWindow(UIWindow *parent, const char *def_dir, const char *def_file, CallbackFn cb) { + if (modal_win == NULL) { + // printf("create modal window\n"); + int w = UIElementMessage(parent, UI_MSG_GET_WIDTH, 0, 0); + int h = UIElementMessage(parent, UI_MSG_GET_HEIGHT, 0, 0); + modal_win = UIWindowCreate(parent, NULL, def_file?"Save File":"Open File", w, h); + modal_win->e.messageUser = WinModalEvent; + UIPanel *panel_out = UIPanelCreate(&modal_win->e, UI_PANEL_GRAY|UI_PANEL_EXPAND); + UILabel *lbl_title = UILabelCreate(&panel_out->e, 0, def_file?"Save File":"Open File", -1); + UIPanel *panel_top = UIPanelCreate(&panel_out->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL); + UITextbox *path_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_DISABLED|UI_ELEMENT_H_FILL); + if (def_dir != NULL) { + UITextboxReplace(path_input, def_dir, -1, false); + } + + UITextbox *filename_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_TAB_STOP|UI_ELEMENT_H_FILL); + + printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input); + if (def_file != NULL) { + UITextboxReplace(filename_input, def_file, -1, false); + } + + UIButton *btn_save = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, def_file?"Save":"Open", -1); + btn_save->e.messageUser = cb; + + UIButton *btn_cancel = UIButtonCreate(&panel_top->e, UI_ELEMENT_TAB_STOP, "Cancel", -1); + btn_cancel->e.messageUser = ButtonCloseEvent; + + UITable *table = UITableCreate(&panel_out->e, UI_ELEMENT_V_FILL, "Directory"); + table->itemCount = 1; // at least '..' element + // @todo this way setting the initial directory is kinda trashcan + char *init_path = (char *)malloc(2*sizeof(char)); + init_path[0] = '.'; + init_path[1] = '\0'; + table->e.cp = (void *)init_path; + table->e.messageUser = TableEvent; + UITableResizeColumns(table); + } else { + UIElementFocus(modal_win); + } +} + +int ButtonSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_CLICKED) { + // printf("save button clicked\n"); + char fname[strlen(img->filename)+7]; + int n = snprintf(fname, NAME_MAX, "atlas_%s", img->filename); + assert(n>0); + ShowModalWindow(element->window, img->dirname, fname, ButtonDialogSaveEvent); + } + return 0; +} + +int ButtonOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_CLICKED) { + // printf("open button clicked\n"); + ShowModalWindow(element->window, getenv("HOME"), NULL, ButtonDialogOpenEvent); + } + return 0; +} + +int SliderEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_VALUE_CHANGED) { + if (img == NULL) return 1; + + float slider_pos = ((UISlider *)element)->position; + float step = 1.0f / (float)img->frame_count; + int new_cols = (int)(slider_pos / step); + if (new_cols > 0 && cols != new_cols) { + //printf("new_cols: %d\n", new_cols); + cols = new_cols; + PreviewUpdate(img, element->cp); + } + } + + return 0; +} + +int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp) { + if (msg == UI_MSG_DESTROY) { + // printf("bye\n"); + free(img); + exit(0); + } + return 0; +} + +void PreviewUpdate(Animation *img, UIImageDisplay *img_disp) { + // gen new spritesheet and refresh the preview + Spritesheet ss = SpritesheetGen(img, cols); + //printf("spritesheet width: %d, height: %d, stride: %d, len: %zu\n", ss.width, ss.height, ss.stride, ss.len); + + uint32_t *frame0 = calloc(ss.width*ss.height, sizeof(uint32_t)); + assert(frame0!=NULL); + + for (uint32_t i=0; i<ss.width*ss.height; ++i) { + frame0[i] = UI_COLOR_FROM_RGBA(ss.data[i*4+0], ss.data[i*4+1], ss.data[i*4+2], ss.data[i*4+3]); + } + UIImageDisplaySetContent(img_disp, frame0, ss.width, ss.height, ss.stride); + UIElementRefresh(img_disp->e.parent); + UIElementRefresh(&img_disp->e); + + free(frame0); + free(ss.data); + + UILabel *label = (UILabel *)img_disp->e.cp; + char label_text[256] = {0}; + snprintf(&label_text, 255, "Width: %3d tiles", cols); + UILabelSetContent(label, label_text, -1); + UIElementRefresh(label->e.parent); +} diff --git a/gui/ui.h b/gui/ui.h @@ -0,0 +1,28 @@ +/* user interface */ +#pragma once + +#ifdef _WIN32 +#define DIR_SEPARATOR '\\' +#else +#define DIR_SEPARATOR '/' +#endif + +#include "luigi.h" + +#define UI_COLOR_FROM_RGBA(r, g, b, a) (((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | ((uint32_t) (b) << 0) | ((uint32_t) (a) << 24)) + +typedef int (*CallbackFn)(struct UIElement *element, UIMessage message, int di, void *dp); + +UIWindow * MainWindowCreate(const char *wname, int w, int h); + +void PreviewUpdate(Animation *, UIImageDisplay *); +void ShowModalWindow(UIWindow *parent, const char *def_dir, const char *def_file, CallbackFn cb); +int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp); +int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp); +int SliderEvent(UIElement *element, UIMessage msg, int di, void *dp); +int ButtonOpenEvent(UIElement *element, UIMessage msg, int di, void *dp); +int ButtonSaveEvent(UIElement *element, UIMessage msg, int di, void *dp); +int ButtonCloseEvent(UIElement *element, UIMessage msg, int di, void *dp); +int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp); +int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp); +int TableEvent(UIElement *element, UIMessage msg, int di, void *dp);