floooh / sokol

minimal cross-platform standalone C headers
https://floooh.github.io/sokol-html5
zlib License
7.12k stars 501 forks source link

How to save a file (download it) in wasm ? #1118

Closed cschar closed 1 month ago

cschar commented 1 month ago

Is it possible to save (download) a file when targeting web platform?

I can write to my filesystem when i compile it natively (MacOS M1)

using the starterkit... LINK: https://github.com/floooh/cimgui-sokol-starterkit

I've modified it only slightly by adding a single FILE writing function + button to trigger it

demo.c

//------------------------------------------------------------------------------
//  Simple C99 cimgui+sokol starter project for Win32, Linux and macOS.
//------------------------------------------------------------------------------
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_log.h"
#include "sokol_glue.h"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#include "cimgui.h"
#include "sokol_imgui.h"

// custom
#include "stdio.h"
#include "stdlib.h"
#include "time.h"

static struct {
    sg_pass_action pass_action;
} state;

static void init(void) {
    sg_setup(&(sg_desc){
        .environment = sglue_environment(),
        .logger.func = slog_func,
    });
    simgui_setup(&(simgui_desc_t){ 0 });

    // initial clear color
    state.pass_action = (sg_pass_action) {
        .colors[0] = { .load_action = SG_LOADACTION_CLEAR, .clear_value = { 0.0f, 0.5f, 1.0f, 1.0 } }
    };
}

// NON BOILERPLATE CODE
void save_my_file(){
    // Seed random number generator
    srand((unsigned int)time(NULL));

    int random_number = rand();

    char filename[50];
    snprintf(filename, sizeof(filename), "foo%d.txt", random_number);

    FILE *file = fopen(filename, "w");
    if (file) {
        fprintf(file, "foo");
        fclose(file);
        printf("File saved: %s\n", filename);  // Debug print
    } else {
        printf("Failed to create file: %s\n", filename);
    }
}
/// END 

static void frame(void) {
    simgui_new_frame(&(simgui_frame_desc_t){
        .width = sapp_width(),
        .height = sapp_height(),
        .delta_time = sapp_frame_duration(),
        .dpi_scale = sapp_dpi_scale(),
    });

    /*=== UI CODE STARTS HERE ===*/
    igSetNextWindowPos((ImVec2){10,10}, ImGuiCond_Once, (ImVec2){0,0});
    igSetNextWindowSize((ImVec2){400, 100}, ImGuiCond_Once);
    igBegin("Hello Dear ImGui!", 0, ImGuiWindowFlags_None);
    igColorEdit3("Background", &state.pass_action.colors[0].clear_value.r, ImGuiColorEditFlags_None);

    // MY CUSTOM FILE SAVER BUTTON
    if (igButton("file saver button", (ImVec2){150,40})){
        save_my_file();
    }

    igEnd();

    /*=== UI CODE ENDS HERE ===*/

    sg_begin_pass(&(sg_pass){ .action = state.pass_action, .swapchain = sglue_swapchain() });
    simgui_render();
    sg_end_pass();
    sg_commit();
}

static void cleanup(void) {
    simgui_shutdown();
    sg_shutdown();
}

static void event(const sapp_event* ev) {
    simgui_handle_event(ev);
}

sapp_desc sokol_main(int argc, char* argv[]) {
    (void)argc;
    (void)argv;
    return (sapp_desc){
        .init_cb = init,
        .frame_cb = frame,
        .cleanup_cb = cleanup,
        .event_cb = event,
        .window_title = "Hello Sokol + Dear ImGui",
        .width = 800,
        .height = 600,
        .icon.sokol_default = true,
        .logger.func = slog_func,
    };
}

When clicking the "File Saver Button" button I get the following error in browser

Browser is GoogleChrome Version 129.0.6668.90 (Official Build) (arm64)

Screenshot 2024-10-07 at 9 10 21 PM

Thanks!

floooh commented 1 month ago

(deleted my previous comment because I misunderstood your question, I thought you wanted to download a file from a server, not from the webpage to the local file system).

Long story short, the sokol headers have no support for saving a file to the local filesystem, and the Emscripten C stdlib won't help you either AFAIK.

You'll need to write your own Javascript code which either creates and 'clicks' a download link (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download), I do this here in the visual6502remix project: https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/src/util.c#L40-L54) - since this emulates a click you'll need to call this function from inside a 'short-lived HTML event handler' though.

The other option is the new-ish HTML filesystem API:

https://developer.mozilla.org/en-US/docs/Web/API/File_System_API

I haven't used this yet though.

But in any case, welcome to web development, where the simplest things are extremely difficult ;)

cschar commented 1 month ago

Ok found a working solution for anyone in future:

had to modify CMakeLists.txt just a bit to be less restrictive

demo.c

//------------------------------------------------------------------------------
//  Simple C99 cimgui+sokol starter project for Win32, Linux and macOS.
//------------------------------------------------------------------------------
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#include "cimgui.h"
#include "sokol_imgui.h"

// custom
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

static struct {
    sg_pass_action pass_action;

    int value;
} state;

static void init(void) {
    sg_setup(&(sg_desc){
        .environment = sglue_environment(),
        .logger.func = slog_func,
    });
    simgui_setup(&(simgui_desc_t){0});

    state.value = 1;

    // initial clear color
    state.pass_action =
        (sg_pass_action){.colors[0] = {.load_action = SG_LOADACTION_CLEAR,
                                       .clear_value = {0.0f, 0.5f, 1.0f, 1.0}}};
}

// NON BOILERPLATE CODE
#if defined(__EMSCRIPTEN__)

void esmc_create_and_download_file(const char* filename, const char* content) {
    FILE* file = fopen(filename, "w");
    if (file == NULL) {
        printf("Error opening file!\n");
        return;
    }
    fprintf(file, "%s", content);
    fclose(file);

    // Trigger immediate download
    EM_ASM({
        var filename = UTF8ToString($0);
        var content = FS.readFile(filename);
        var blob = new Blob([content], { type: "application/octet-stream" });
        var url = URL.createObjectURL(blob);
        var a = document.createElement("a");
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
    }, filename);
};

#else

static void native_create_and_download_file(const char* filename, const char* content){
    FILE* file = fopen(filename, "w");
    if (file == NULL) {
        printf("Error opening file! \n");
        return;
    }
    fprintf(file, "%s", content);
    fclose(file);

    printf("saved %s\n", filename);
}

#endif

void save_my_file() {
    char content[100];
    sprintf(content, "some content with value %d", state.value);
    char filename[100];
    sprintf(filename, "my_textfile_%d.txt", state.value);

    #ifdef __EMSCRIPTEN__
        esmc_create_and_download_file(filename, content);
    #else
        native_create_and_download_file(filename, content);
    #endif

    state.value = state.value + 1;
}
/// END

static void frame(void) {
    simgui_new_frame(&(simgui_frame_desc_t){
        .width = sapp_width(),
        .height = sapp_height(),
        .delta_time = sapp_frame_duration(),
        .dpi_scale = sapp_dpi_scale(),
    });

    /*=== UI CODE STARTS HERE ===*/
    igSetNextWindowPos((ImVec2){10, 10}, ImGuiCond_Once, (ImVec2){0, 0});
    igSetNextWindowSize((ImVec2){400, 100}, ImGuiCond_Once);
    igBegin("Hello Dear ImGui!", 0, ImGuiWindowFlags_None);
    igColorEdit3("Background", &state.pass_action.colors[0].clear_value.r,
                 ImGuiColorEditFlags_None);

    // MY CUSTOM FILE SAVER BUTTON
    if (igButton("file saver button", (ImVec2){150, 40})) {
        save_my_file();
    }

    igEnd();

    /*=== UI CODE ENDS HERE ===*/

    sg_begin_pass(&(sg_pass){.action = state.pass_action,
                             .swapchain = sglue_swapchain()});
    simgui_render();
    sg_end_pass();
    sg_commit();
}

static void cleanup(void) {
    simgui_shutdown();
    sg_shutdown();
}

static void event(const sapp_event* ev) { simgui_handle_event(ev); }

sapp_desc sokol_main(int argc, char* argv[]) {
    (void)argc;
    (void)argv;
    return (sapp_desc){
        .init_cb = init,
        .frame_cb = frame,
        .cleanup_cb = cleanup,
        .event_cb = event,
        .window_title = "Hello Sokol + Dear ImGui",
        .width = 800,
        .height = 600,
        .icon.sokol_default = true,
        .logger.func = slog_func,
    };
}

CMakelists.txt


# other stuff...

# Emscripten-specific linker options
if (CMAKE_SYSTEM_NAME STREQUAL Emscripten)
    set(CMAKE_EXECUTABLE_SUFFIX ".html")
    # use our own minimal shell.html
    target_link_options(demo PRIVATE --shell-file ../sokol/shell.html)
    # link with WebGL2
    target_link_options(demo PRIVATE -sUSE_WEBGL2=1)

    # WASM+JS size optimizations
    # target_link_options(demo PRIVATE -sNO_FILESYSTEM=1 -sASSERTIONS=0 -sMALLOC=emmalloc --closure=1)
    # Disable the above, put less restrictive flag
    target_link_options(demo PRIVATE -sASSERTIONS=0 --closure=1)

endif()

# other stuff...

Clicking button now downloads file!

Screenshot 2024-10-08 at 9 55 14 AM
floooh commented 1 month ago

TBH, I'm surprised that this works:

    if (igButton("file saver button", (ImVec2){150, 40})) {
        save_my_file();
    }

...you might want to check on different browsers, especially Safari, because AFAIK this JS statement:

a.click();

...might need to be called from a JS event handler (for instance from within the sokol_app.h event callback).

cschar commented 1 month ago

Hmmm... only added in an extra print statement in ... seems to be working... guess I"ll count my lucky stars for now haha.

OS: Sonoma 14.6 (23G80) Safari Version 17.6 (19618.3.11.11.5)

https://github.com/user-attachments/assets/c7d36ec1-8fa9-4df6-9b82-8d23044aba6f

demo.c

//------------------------------------------------------------------------------
//  Simple C99 cimgui+sokol starter project for Win32, Linux and macOS.
//------------------------------------------------------------------------------
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#include "cimgui.h"
#include "sokol_imgui.h"

// custom
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

static struct {
    sg_pass_action pass_action;

    int value;
} state;

static void init(void) {
    sg_setup(&(sg_desc){
        .environment = sglue_environment(),
        .logger.func = slog_func,
    });
    simgui_setup(&(simgui_desc_t){0});

    state.value = 1;

    // initial clear color
    state.pass_action =
        (sg_pass_action){.colors[0] = {.load_action = SG_LOADACTION_CLEAR,
                                       .clear_value = {0.0f, 0.5f, 1.0f, 1.0}}};
}

// NON BOILERPLATE CODE
#if defined(__EMSCRIPTEN__)

void esmc_create_and_download_file(const char* filename, const char* content) {
    printf("emsc file save...\n");

    FILE* file = fopen(filename, "w");
    if (file == NULL) {
        printf("Error opening file!\n");
        return;
    }
    fprintf(file, "%s", content);
    fclose(file);

    // Trigger immediate download
    EM_ASM({
        var filename = UTF8ToString($0);
        var content = FS.readFile(filename);
        var blob = new Blob([content], { type: "application/octet-stream" });
        var url = URL.createObjectURL(blob);
        var a = document.createElement("a");
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
    }, filename);
};

#else

static void native_create_and_download_file(const char* filename, const char* content){
    // printf("native file save...\n");

    FILE* file = fopen(filename, "w");
    if (file == NULL) {
        printf("Error opening file! \n");
        return;
    }
    fprintf(file, "%s", content);
    fclose(file);

    printf("saved %s\n", filename);
}

#endif

void save_my_file() {
    char content[100];
    sprintf(content, "some content with value %d", state.value);
    char filename[100];
    sprintf(filename, "my_textfile_%d.txt", state.value);

    #ifdef __EMSCRIPTEN__
        esmc_create_and_download_file(filename, content);
    #else
        native_create_and_download_file(filename, content);
    #endif

    state.value = state.value + 1;
}
/// END

static void frame(void) {
    simgui_new_frame(&(simgui_frame_desc_t){
        .width = sapp_width(),
        .height = sapp_height(),
        .delta_time = sapp_frame_duration(),
        .dpi_scale = sapp_dpi_scale(),
    });

    /*=== UI CODE STARTS HERE ===*/
    igSetNextWindowPos((ImVec2){10, 10}, ImGuiCond_Once, (ImVec2){0, 0});
    igSetNextWindowSize((ImVec2){400, 100}, ImGuiCond_Once);
    igBegin("Hello Dear ImGui!", 0, ImGuiWindowFlags_None);
    igColorEdit3("Background", &state.pass_action.colors[0].clear_value.r,
                 ImGuiColorEditFlags_None);

    // MY CUSTOM FILE SAVER BUTTON
    if (igButton("file saver button", (ImVec2){150, 40})) {
        printf(" button clicked...\n");
        save_my_file();

    }

    igEnd();

    /*=== UI CODE ENDS HERE ===*/

    sg_begin_pass(&(sg_pass){.action = state.pass_action,
                             .swapchain = sglue_swapchain()});
    simgui_render();
    sg_end_pass();
    sg_commit();
}

static void cleanup(void) {
    simgui_shutdown();
    sg_shutdown();
}

static void event(const sapp_event* ev) { simgui_handle_event(ev); }

sapp_desc sokol_main(int argc, char* argv[]) {
    (void)argc;
    (void)argv;
    return (sapp_desc){
        .init_cb = init,
        .frame_cb = frame,
        .cleanup_cb = cleanup,
        .event_cb = event,
        .window_title = "Hello Sokol + Dear ImGui",
        .width = 800,
        .height = 600,
        .icon.sokol_default = true,
        .logger.func = slog_func,
    };
}