From 7ab5c04496c0f65140d84ca297e0d91ed05b0d5a Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Thu, 8 Jul 2021 00:38:20 +0000 Subject: [PATCH] Basic example --- .clang-format | 1 + Makefile | 9 ++ api.cpp | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++ ui.html | 164 +++++++++++++++++++++++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 .clang-format create mode 100644 Makefile create mode 100644 api.cpp create mode 100644 ui.html diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f6cb8ad --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..12e2460 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +CPPFLAGS += -I $(GPHOTO2PREFIX)/include + +libapi.mjs: api.cpp + libtool --verbose --mode=link --tag=CXX $(CC) $+ --bind -s ASYNCIFY -fexceptions -s DYNAMIC_EXECUTION=0 -g -s ASYNCIFY_STACK_SIZE=1048576 -o $@ \ + $(GPHOTO2PREFIX)/lib/libltdl.la \ + $(GPHOTO2PREFIX)/lib/libgphoto2.la \ + -dlpreopen $(GPHOTO2PREFIX)/lib/libgphoto2/2.5.27.1/ptp2.la \ + -dlpreopen $(GPHOTO2PREFIX)/lib/libgphoto2_port/0.12.0/usb1.la \ + $(CPPFLAGS) diff --git a/api.cpp b/api.cpp new file mode 100644 index 0000000..0e1a43d --- /dev/null +++ b/api.cpp @@ -0,0 +1,246 @@ +#include +#include +#include +#include + +#include + +using emscripten::val; + +void gpp_try(int status) { + if (status != GP_OK) { + throw std::runtime_error(gp_result_as_string(status)); + } +} + +void gpp_error(GPContext *context, const char *text, void *data) { + throw std::runtime_error(text); +} + +#define GPP_CALL(RET, EXPR) \ + ({ \ + RET OUT; \ + RET *_ = &OUT; \ + gpp_try(EXPR); \ + OUT; \ + }) + +EM_JS([[noreturn]] void, throw_msg, (const char *msg), + { throw new Error(UTF8ToString(msg)); }); + +template +auto gpp_rethrow(Func func) { + try { + return func(); + } catch (std::exception &e) { + throw_msg(e.what()); + } +} + +const thread_local val Uint8Array = val::global("Uint8Array"); +const thread_local val Blob = val::global("Blob"); + +class Context { + public: + Context() + : camera(nullptr, gp_camera_unref), + context(nullptr, gp_context_unref), + config(nullptr, gp_widget_unref) { + gpp_rethrow([=]() { + camera.reset(GPP_CALL(Camera *, gp_camera_new(_))); + context.reset(gp_context_new()); + gp_context_set_error_func(context.get(), gpp_error, nullptr); + gpp_try(gp_camera_init(camera.get(), context.get())); + }); + } + + Context(int foo): Context() {} + + val configToJS() { + return gpp_rethrow([=]() { + if (config.get() == nullptr) { + config.reset( + GPP_CALL(CameraWidget *, + gp_camera_get_config(camera.get(), _, context.get()))); + } + + return walk_config(config.get()); + }); + } + + void setConfigValue(int id, val value) { + gpp_rethrow([=]() { + auto widget = GPP_CALL(CameraWidget *, + gp_widget_get_child_by_id(config.get(), id, _)); + auto type = GPP_CALL(CameraWidgetType, gp_widget_get_type(widget, _)); + switch (type) { + case GP_WIDGET_RANGE: { + float number = value.as(); + gpp_try(gp_widget_set_value(widget, &number)); + break; + } + case GP_WIDGET_MENU: + case GP_WIDGET_RADIO: + case GP_WIDGET_TEXT: { + auto str = value.as(); + gpp_try(gp_widget_set_value(widget, str.c_str())); + break; + } + case GP_WIDGET_TOGGLE: { + int on = value.as() ? 1 : 0; + gpp_try(gp_widget_set_value(widget, &on)); + break; + } + default: { + throw std::logic_error("unimplemented"); + } + } + gpp_try(gp_camera_set_config(camera.get(), config.get(), context.get())); + }); + } + + val capturePreviewAsBlob() { + return gpp_rethrow([=]() { + class Output { + public: + Output() : totalSize(0), chunks(val::array()) {} + + void push(const uint8_t *data, unsigned long size) { + chunks.call( + "push", + Uint8Array.new_(emscripten::typed_memory_view(size, data))); + totalSize += size; + } + + uint64_t getTotalSize() { return totalSize; } + + val getChunks() { return chunks; } + + private: + uint64_t totalSize; + val chunks; + }; + + static CameraFileHandler handler = { + .size = + [](void *priv, uint64_t *size) { + *size = static_cast(priv)->getTotalSize(); + return GP_OK; + }, + .read = [](void *priv, unsigned char *data, + uint64_t *len) { return GP_ERROR_NOT_SUPPORTED; }, + .write = + [](void *priv, unsigned char *data, uint64_t *len) { + static_cast(priv)->push(data, *len); + return GP_OK; + }}; + + Output output; + + std::unique_ptr file( + GPP_CALL(CameraFile *, + gp_file_new_from_handler(_, &handler, &output)), + gp_file_unref); + + gpp_try( + gp_camera_capture_preview(camera.get(), file.get(), context.get())); + + auto mime_type = + GPP_CALL(const char *, gp_file_get_mime_type(file.get(), _)); + + val blob_opts = val::object(); + blob_opts.set("type", mime_type); + + return Blob.new_(output.getChunks(), std::move(blob_opts)); + }); + } + + private: + std::unique_ptr camera; + std::unique_ptr context; + std::unique_ptr config; + + static val walk_config(CameraWidget *widget) { + val result = val::object(); + + result.set("id", GPP_CALL(int, gp_widget_get_id(widget, _))); + result.set("name", GPP_CALL(const char *, gp_widget_get_name(widget, _))); + result.set("info", GPP_CALL(const char *, gp_widget_get_info(widget, _))); + result.set("label", GPP_CALL(const char *, gp_widget_get_label(widget, _))); + result.set("readonly", + GPP_CALL(int, gp_widget_get_readonly(widget, _)) != 0); + + auto type = GPP_CALL(CameraWidgetType, gp_widget_get_type(widget, _)); + + switch (type) { + case GP_WIDGET_RANGE: { + result.set("type", "range"); + result.set("value", GPP_CALL(float, gp_widget_get_value(widget, _))); + + float min, max, step; + gpp_try(gp_widget_get_range(widget, &min, &max, &step)); + result.set("min", min); + result.set("max", max); + result.set("step", step); + + break; + } + case GP_WIDGET_MENU: + case GP_WIDGET_RADIO: { + result.set("type", type == GP_WIDGET_MENU ? "menu" : "radio"); + result.set("value", + GPP_CALL(const char *, gp_widget_get_value(widget, _))); + + val choices = val::array(); + for (int i = 0, n = gp_widget_count_choices(widget); i < n; i++) { + choices.call( + "push", + val(GPP_CALL(const char *, gp_widget_get_choice(widget, i, _)))); + } + result.set("choices", choices); + + break; + } + case GP_WIDGET_TOGGLE: { + result.set("type", "toggle"); + result.set("value", GPP_CALL(int, gp_widget_get_value(widget, _)) != 0); + + break; + } + case GP_WIDGET_TEXT: { + result.set("type", "text"); + result.set("value", + GPP_CALL(const char *, gp_widget_get_value(widget, _))); + + break; + } + case GP_WIDGET_WINDOW: + case GP_WIDGET_SECTION: { + result.set("type", type == GP_WIDGET_WINDOW ? "window" : "section"); + + val children = val::array(); + for (int i = 0, n = gp_widget_count_children(widget); i < n; i++) { + auto child = + GPP_CALL(CameraWidget *, gp_widget_get_child(widget, i, _)); + children.call("push", walk_config(child)); + } + result.set("children", children); + + break; + } + default: { + throw std::logic_error("unimplemented"); + } + } + + return result; + } +}; + +EMSCRIPTEN_BINDINGS(gphoto2_js_api) { + emscripten::class_("Context") + .constructor<>() + .function("configToJS", &Context::configToJS) + .function("setConfigValue", &Context::setConfigValue) + .function("capturePreviewAsBlob", &Context::capturePreviewAsBlob); +} diff --git a/ui.html b/ui.html new file mode 100644 index 0000000..7a918ff --- /dev/null +++ b/ui.html @@ -0,0 +1,164 @@ + + +
+