From 875a3760ed2b96f4e920949775b4f11bf8ee3039 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Mon, 5 Jan 2026 19:15:42 +0300 Subject: [PATCH] Paste images from clipboard added --- .../emscripten_browser_clipboard.h | 48 ++++++++++++ src/DrawingProgram/DrawingProgram.cpp | 76 +++++++++++++------ .../Layers/DrawingProgramLayerManager.cpp | 5 +- src/InputManager.cpp | 27 +++++++ src/InputManager.hpp | 1 + src/Toolbar.cpp | 13 +++- 6 files changed, 142 insertions(+), 28 deletions(-) diff --git a/include/EmscriptenHelpers/emscripten_browser_clipboard.h b/include/EmscriptenHelpers/emscripten_browser_clipboard.h index 50e0560..5cb062e 100644 --- a/include/EmscriptenHelpers/emscripten_browser_clipboard.h +++ b/include/EmscriptenHelpers/emscripten_browser_clipboard.h @@ -17,6 +17,7 @@ namespace emscripten_browser_clipboard { /////////////////////////////////// Interface ////////////////////////////////// +using paste_image_handler = void(*)(std::string, void*); using paste_handler = void(*)(std::string&&, void*); using copy_handler = char const*(*)(void*); @@ -48,6 +49,42 @@ EM_JS_INLINE(void, paste_async_js, (paste_handler callback, void *callback_data) }); }); +EM_JS_INLINE(void, paste_async_image_js, (paste_image_handler callback, void *callback_data), { + /// Register the given callback to handle paste events. Callback data pointer is passed through to the callback. + /// Paste handler callback signature is: + /// void my_handler(std::string const &paste_data, void *callback_data = nullptr); + navigator.clipboard.read() + .then(clipboardContents => { + const imageMimetypesSupported = ["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]; + for (const item of clipboardContents) { + for(const mimeType of imageMimetypesSupported) { + if(item.types.includes(mimeType)) { + item.getType(mimeType).then(blob => { + const file_reader = new FileReader(); + file_reader.onload = (event) => { + const uint8Arr = new Uint8Array(event.target.result); + const data_ptr = Module["_malloc"](uint8Arr.length); + const data_on_heap = new Uint8Array(Module["HEAPU8"].buffer, data_ptr, uint8Arr.length); + data_on_heap.set(uint8Arr); + Module["ccall"]('emscripten_browser_clipboard_detail_paste_image_return', 'number', ['number', 'number', 'number', 'number'], [data_on_heap.byteOffset, uint8Arr.length, callback, callback_data]); + Module["_free"](data_ptr); + }; + file_reader.readAsArrayBuffer(blob); + }) + .catch(err => { + console.error('Failed to get blob from clipboard item: ', err); + }); + return; + } + } + } + console.log('Could not find image mimetype in clipboard'); + }) + .catch(err => { + console.error('Failed to read clipboard contents: ', err); + }); +}); + EM_JS_INLINE(void, copy_js, (copy_handler callback, void *callback_data), { /// Register the given callback to handle copy events. Callback data pointer is passed through to the callback. /// Copy handler callback signature is: @@ -76,6 +113,11 @@ inline void paste_async(paste_handler callback, void *callback_data) { detail::paste_async_js(callback, callback_data); } +inline void paste_async_image(paste_image_handler callback, void *callback_data) { + /// C++ wrapper for javascript paste call + detail::paste_async_image_js(callback, callback_data); +} + inline void copy(copy_handler callback, void *callback_data) { /// C++ wrapper for javascript copy call detail::copy_js(callback, callback_data); @@ -98,6 +140,12 @@ EMSCRIPTEN_KEEPALIVE inline int emscripten_browser_clipboard_detail_paste_return return 1; } +EMSCRIPTEN_KEEPALIVE inline int emscripten_browser_clipboard_detail_paste_image_return(char const *pasteData, int pasteSize, paste_image_handler callback, void *callback_data) { + /// Call paste callback - this function is called from javascript when the paste event occurs + callback(std::string(pasteData, pasteSize), callback_data); + return 1; +} + EMSCRIPTEN_KEEPALIVE inline char const *emscripten_browser_clipboard_detail_copy_return(copy_handler callback, void *callback_data); EMSCRIPTEN_KEEPALIVE inline char const *emscripten_browser_clipboard_detail_copy_return(copy_handler callback, void *callback_data) { diff --git a/src/DrawingProgram/DrawingProgram.cpp b/src/DrawingProgram/DrawingProgram.cpp index 9c43499..c51ff76 100644 --- a/src/DrawingProgram/DrawingProgram.cpp +++ b/src/DrawingProgram/DrawingProgram.cpp @@ -36,6 +36,10 @@ #include "Tools/EraserTool.hpp" +#ifdef __EMSCRIPTEN__ + #include +#endif + DrawingProgram::DrawingProgram(World& initWorld): world(initWorld), drawCache(*this), @@ -166,6 +170,30 @@ bool DrawingProgram::selection_action_menu(Vector2f popupPos) { selection.paste_clipboard(popupPos * t.final_gui_scale()); shouldClose = true; } + if(t.gui.text_button_left_transparent("Paste Image", "Paste Image")) { + #ifdef __EMSCRIPTEN__ + struct PasteData { + std::weak_ptr w; + Vector2f screenPos; + }; + static PasteData pasteData; + pasteData.screenPos = popupPos; + pasteData.w = make_weak_ptr(world.main.world); + emscripten_browser_clipboard::paste_async_image([](std::string pasteData, void* callbackData){ + PasteData* p = (PasteData*)callbackData; + std::shared_ptr wLock = p->w.lock(); + if(wLock) + wLock->drawProg.add_file_to_canvas_by_data("Image from clipboard", std::string(pasteData), p->screenPos); + else + Logger::get().log("INFO", "Loading image to canvas that has been destroyed"); + }, &pasteData); + #else + std::string imageData = world.main.input.get_clipboard_image_data_SDL(); + if(!imageData.empty()) + add_file_to_canvas_by_data("Image from clipboard", imageData, popupPos); + #endif + shouldClose = true; + } if(selection.is_something_selected()) { if(t.gui.text_button_left_transparent("Copy", "Copy")) { selection.selection_to_clipboard(); @@ -486,9 +514,32 @@ void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& fil } void DrawingProgram::add_file_to_canvas_by_path_execute(const std::filesystem::path& filePath, Vector2f dropPos) { - NetworkingObjects::NetObjTemporaryPtr imageTempPtr = world.rMan.add_resource_file(filePath); - if(imageTempPtr) { - NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id(); + if(layerMan.is_a_layer_being_edited()) { + NetworkingObjects::NetObjTemporaryPtr imageTempPtr = world.rMan.add_resource_file(filePath); + if(imageTempPtr) { + NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id(); + ResourceDisplay* display = world.rMan.get_display_data(imageID); + Vector2f imTrueDim = display->get_dimensions(); + CanvasComponentContainer* newContainer = new CanvasComponentContainer(world.netObjMan, CanvasComponentType::IMAGE); + ImageCanvasComponent& img = static_cast(newContainer->get_comp()); + newContainer->coords = world.drawData.cam.c; + float imWidth = imTrueDim.x() / (imTrueDim.x() + imTrueDim.y()); + float imHeight = imTrueDim.y() / (imTrueDim.x() + imTrueDim.y()); + Vector2f imDim = Vector2f{world.main.window.size.x() * imWidth, world.main.window.size.x() * imHeight} * display->get_dimension_scale(); + img.d.p1 = dropPos - imDim; + img.d.p2 = dropPos + imDim; + img.d.imageID = imageID; + layerMan.add_component_to_layer_being_edited(newContainer); + } + } +} + +void DrawingProgram::add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos) { + if(layerMan.is_a_layer_being_edited()) { + ResourceData newResource; + newResource.data = std::make_shared(fileBuffer); + newResource.name = fileName; + NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id(); ResourceDisplay* display = world.rMan.get_display_data(imageID); Vector2f imTrueDim = display->get_dimensions(); CanvasComponentContainer* newContainer = new CanvasComponentContainer(world.netObjMan, CanvasComponentType::IMAGE); @@ -504,25 +555,6 @@ void DrawingProgram::add_file_to_canvas_by_path_execute(const std::filesystem::p } } -void DrawingProgram::add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos) { - ResourceData newResource; - newResource.data = std::make_shared(fileBuffer); - newResource.name = fileName; - NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id(); - ResourceDisplay* display = world.rMan.get_display_data(imageID); - Vector2f imTrueDim = display->get_dimensions(); - CanvasComponentContainer* newContainer = new CanvasComponentContainer(world.netObjMan, CanvasComponentType::IMAGE); - ImageCanvasComponent& img = static_cast(newContainer->get_comp()); - newContainer->coords = world.drawData.cam.c; - float imWidth = imTrueDim.x() / (imTrueDim.x() + imTrueDim.y()); - float imHeight = imTrueDim.y() / (imTrueDim.x() + imTrueDim.y()); - Vector2f imDim = Vector2f{world.main.window.size.x() * imWidth, world.main.window.size.x() * imHeight} * display->get_dimension_scale(); - img.d.p1 = dropPos - imDim; - img.d.p2 = dropPos + imDim; - img.d.imageID = imageID; - layerMan.add_component_to_layer_being_edited(newContainer); -} - float DrawingProgram::drag_point_radius() { return 8.0f * world.main.toolbar.final_gui_scale(); } diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp b/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp index afe119d..c8e0fe0 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp @@ -99,7 +99,7 @@ std::vector DrawingProgramLayerManage if(!newObjs.empty()) { auto editLayerPtr = editingLayer.lock(); if(!editLayerPtr) - return {}; + throw std::runtime_error("[DrawingProgramLayerManager::add_many_components_to_layer_being_edited] No layer selected!"); auto toRet = editLayerPtr->get_layer().components->insert_ordered_list_and_send_create(editLayerPtr->get_layer().components, newObjs); add_undo_place_components(editingLayer.lock().get(), toRet); } @@ -109,11 +109,10 @@ std::vector DrawingProgramLayerManage CanvasComponentContainer::ObjInfo* DrawingProgramLayerManager::add_component_to_layer_being_edited(CanvasComponentContainer* newObj) { auto editLayerPtr = editingLayer.lock(); if(!editLayerPtr) - return nullptr; + throw std::runtime_error("[DrawingProgramLayerManager::add_component_to_layer_being_edited] No layer selected!"); return &(*editLayerPtr->get_layer().components->push_back_and_send_create(editLayerPtr->get_layer().components, newObj)); } - void DrawingProgramLayerManager::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version) { if(version >= VersionNumber(0, 4, 0)) { layerTreeRoot = drawP.world.netObjMan.make_obj_from_ptr(new DrawingProgramLayerListItem()); diff --git a/src/InputManager.cpp b/src/InputManager.cpp index 1a458ea..55f35d9 100644 --- a/src/InputManager.cpp +++ b/src/InputManager.cpp @@ -136,6 +136,33 @@ std::string InputManager::get_clipboard_str_SDL() { return toRet; } +std::string InputManager::get_clipboard_image_data_SDL() { +#ifndef __EMSCRIPTEN__ + static std::unordered_set validMimetypes; + if(validMimetypes.empty()) { + validMimetypes.emplace("image/png"); + validMimetypes.emplace("image/gif"); + validMimetypes.emplace("image/jpeg"); + validMimetypes.emplace("image/svg+xml"); + validMimetypes.emplace("image/webp"); + } + size_t mimeTypeSize; + char** mimeTypes = SDL_GetClipboardMimeTypes(&mimeTypeSize); + if(mimeTypes) { + for(size_t i = 0; i < mimeTypeSize; i++) { + if(validMimetypes.contains(mimeTypes[i])) { + size_t clipboardDataSize; + void* clipboardData = SDL_GetClipboardData(mimeTypes[i], &clipboardDataSize); + std::string toRet(static_cast(clipboardData), clipboardDataSize); + SDL_free(clipboardData); + return toRet; + } + } + } +#endif + return std::string(); +} + void InputManager::set_clipboard_str(std::string_view s) { #ifdef __EMSCRIPTEN__ emscripten_browser_clipboard::copy(std::string(s)); diff --git a/src/InputManager.hpp b/src/InputManager.hpp index 4fe3d06..491678c 100644 --- a/src/InputManager.hpp +++ b/src/InputManager.hpp @@ -211,6 +211,7 @@ struct InputManager { void set_clipboard_str(std::string_view s); void set_clipboard_plain_and_richtext_pair(const std::pair& plainAndRichtextPair); std::string get_clipboard_str_SDL(); + std::string get_clipboard_image_data_SDL(); void call_text_paste(bool isRichTextPaste); void process_text_paste(const std::string& plainClipboardStr); diff --git a/src/Toolbar.cpp b/src/Toolbar.cpp index fb7d650..bb57409 100644 --- a/src/Toolbar.cpp +++ b/src/Toolbar.cpp @@ -541,17 +541,24 @@ void Toolbar::top_toolbar() { main.world->drawProg.switch_to_tool(DrawingProgramToolType::SCREENSHOT); if(gui.text_button_left_transparent("add image or file to canvas", "Add Image/File to Canvas")) { #ifdef __EMSCRIPTEN__ + static std::weak_ptr worldWeakPtr; + worldWeakPtr = make_weak_ptr(main.world); emscripten_browser_file::upload("*", [](std::string const& fileName, std::string const& mimeType, std::string_view buffer, void* callbackData) { if(!buffer.empty()) { - World* world = (World*)callbackData; - world->drawProg.add_file_to_canvas_by_data(fileName, buffer, world->main.window.size.cast() / 2.0f); + auto world = ((std::weak_ptr*)callbackData)->lock(); + if(world) + world->drawProg.add_file_to_canvas_by_data(fileName, buffer, world->main.window.size.cast() / 2.0f); + else + Logger::get().log("INFO", "Loading image to canvas that has been destroyed"); } - }, main.world.get()); + }, &worldWeakPtr); #else open_file_selector("Open File", {{"Any File", "*"}}, [w = make_weak_ptr(main.world)](const std::filesystem::path& p, const auto& e) { auto wLock = w.lock(); if(wLock) wLock->drawProg.add_file_to_canvas_by_path(p.string(), wLock->main.window.size.cast() / 2.0f, false); + else + Logger::get().log("INFO", "Loading image to canvas that has been destroyed"); }); #endif }