// Based on https://github.com/msorvig/qt-webassembly-examples/emscripten_localfiles #include <emscripten/bind.h> #include "common_macros.h" #include "EmscriptenLocalFile.h" namespace ncine { namespace { using FileDataCallbackType = void(void *context, char *data, size_t length, const char *name); using LoadingCallbackType = void(void *context); void readFileContent(emscripten::val event) { // Copy file content to WebAssembly memory and call the user file data handler emscripten::val fileReader = event["target"]; // Set up source typed array emscripten::val result = fileReader["result"]; // ArrayBuffer emscripten::val Uint8Array = emscripten::val::global("Uint8Array"); emscripten::val sourceTypedArray = Uint8Array.new_(result); // Allocate and set up destination typed array const size_t size = result["byteLength"].as<size_t>(); char *buffer = new char[size]; emscripten::val destinationTypedArray = Uint8Array.new_(emscripten::val::module_property("HEAPU8")["buffer"], size_t(buffer), size); destinationTypedArray.call<void>("set", sourceTypedArray); // Call user file data handler FileDataCallbackType *fileDataCallback = reinterpret_cast<FileDataCallbackType *>(fileReader["data-filesReadyCallback"].as<size_t>()); void *context = reinterpret_cast<void *>(fileReader["data-callbackContext"].as<size_t>()); fileDataCallback(context, buffer, size, fileReader["data-name"].as<std::string>().c_str()); } void readFiles(emscripten::val event) { // Read all selected files using FileReader emscripten::val target = event["target"]; emscripten::val files = target["files"]; const int fileCount = files["length"].as<int>(); if (fileCount > 0) { LoadingCallbackType *loadingCallback = reinterpret_cast<LoadingCallbackType *>(target["data-loadingCallback"].as<size_t>()); void *context = reinterpret_cast<void *>(target["data-callbackContext"].as<size_t>()); loadingCallback(context); } for (int i = 0; i < fileCount; i++) { emscripten::val file = files[i]; emscripten::val fileReader = emscripten::val::global("FileReader").new_(); fileReader.set("onload", emscripten::val::module_property("jsReadFileContent")); fileReader.set("data-filesReadyCallback", target["data-filesReadyCallback"]); fileReader.set("data-callbackContext", target["data-callbackContext"]); fileReader.set("data-name", file["name"]); fileReader.call<void>("readAsArrayBuffer", file); } } /// Loads a file by opening a native file dialog void loadFile(const char *accept, FileDataCallbackType *filesReadyCallback, LoadingCallbackType *loadingCallback, void *context) { // Create file input element which will display a native file dialog. emscripten::val document = emscripten::val::global("document"); emscripten::val input = document.call<emscripten::val>("createElement", std::string("input")); input.set("type", "file"); input.set("style", "display:none"); input.set("accept", emscripten::val(accept)); // Set JavaScript `onchange` callback which will be called on selected file(s), // and also forward the user C callback pointers so that the `onchange` // callback can call it. (The `onchange` callback is actually a C function // exposed to JavaScript with EMSCRIPTEN_BINDINGS). input.set("onchange", emscripten::val::module_property("jsReadFiles")); input.set("data-filesReadyCallback", emscripten::val(size_t(filesReadyCallback))); input.set("data-loadingCallback", emscripten::val(size_t(loadingCallback))); input.set("data-callbackContext", emscripten::val(size_t(context))); // Programatically activate input emscripten::val body = document["body"]; body.call<void>("appendChild", input); input.call<void>("click"); body.call<void>("removeChild", input); } /// Saves file by triggering a browser file download. void saveFile(const char *data, size_t length, const char *filenameHint) { // Create file data Blob emscripten::val Blob = emscripten::val::global("Blob"); emscripten::val contentArray = emscripten::val::array(); emscripten::val content = emscripten::val(emscripten::typed_memory_view(length, data)); contentArray.call<void>("push", content); emscripten::val type = emscripten::val::object(); type.set("type", "application/octet-stream"); emscripten::val fileBlob = Blob.new_(contentArray, type); // Create Blob download link emscripten::val document = emscripten::val::global("document"); emscripten::val link = document.call<emscripten::val>("createElement", std::string("a")); link.set("download", filenameHint); emscripten::val window = emscripten::val::global("window"); emscripten::val URL = window["URL"]; link.set("href", URL.call<emscripten::val>("createObjectURL", fileBlob)); link.set("style", "display:none"); // Programatically click link emscripten::val body = document["body"]; body.call<void>("appendChild", link); link.call<void>("click"); body.call<void>("removeChild", link); } EMSCRIPTEN_BINDINGS(localfileaccess) { function("jsReadFiles", &readFiles); function("jsReadFileContent", &readFileContent); }; } /////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS /////////////////////////////////////////////////////////// void EmscriptenLocalFile::load() { loadFile("*", fileDataCallback, loadingCallback, this); } void EmscriptenLocalFile::load(const char *fileFilter) { FATAL_ASSERT(fileFilter); loadFile(fileFilter, fileDataCallback, loadingCallback, this); } void EmscriptenLocalFile::save(const char *filename) { FATAL_ASSERT(filename); ASSERT(fileSize_ > 0); LOGI_X("Saving file: \"%s\" (%u bytes)", filename, fileSize_); saveFile(fileBuffer_.get(), fileSize_, filename); } unsigned long int EmscriptenLocalFile::read(void *buffer, unsigned long int bytes) const { FATAL_ASSERT(buffer); ASSERT(bytes > 0); memcpy(buffer, fileBuffer_.get(), bytes); return bytes; } unsigned long int EmscriptenLocalFile::write(void *buffer, unsigned long int bytes) { FATAL_ASSERT(buffer); ASSERT(bytes > 0); fileBuffer_ = nctl::makeUnique<char[]>(bytes); memcpy(fileBuffer_.get(), buffer, bytes); fileSize_ = bytes; return bytes; } void EmscriptenLocalFile::setLoadedCallback(LoadedCallbackType *loadedCallback, void *userData) { loadedCallback_ = loadedCallback; userData_ = userData; } /////////////////////////////////////////////////////////// // PRIVATE FUNCTIONS /////////////////////////////////////////////////////////// void EmscriptenLocalFile::fileDataCallback(void *context, char *contentPointer, size_t contentSize, const char *filename) { FATAL_ASSERT(context); FATAL_ASSERT(contentPointer); ASSERT(contentSize > 0); LOGI_X("Loading file: \"%s\" (%u bytes)", filename, contentSize); EmscriptenLocalFile *localFile = reinterpret_cast<EmscriptenLocalFile *>(context); localFile->fileBuffer_ = nctl::makeUnique<char[]>(contentSize); memcpy(localFile->fileBuffer_.get(), contentPointer, contentSize); delete[](contentPointer); localFile->fileSize_ = contentSize; localFile->filename_ = filename; localFile->loading_ = false; if (localFile->loadedCallback_) localFile->loadedCallback_(*localFile, localFile->userData_); } void EmscriptenLocalFile::loadingCallback(void *context) { FATAL_ASSERT(context); EmscriptenLocalFile *localFile = reinterpret_cast<EmscriptenLocalFile *>(context); localFile->loading_ = true; } }