diff --git a/CMakeLists.txt b/CMakeLists.txt index 7edcd2e..7e148be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 2.8) project(owo) add_executable(owo owo.cpp) -add_subdirectory(clip) +add_subdirectory(clip-lib) target_link_libraries(owo clip) diff --git a/clip b/clip deleted file mode 160000 index 4e9eeea..0000000 --- a/clip +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e9eeea9293c6f35a1ebac0fd66db7463890f143 diff --git a/clip-lib/CMakeLists.txt b/clip-lib/CMakeLists.txt new file mode 100644 index 0000000..d72d150 --- /dev/null +++ b/clip-lib/CMakeLists.txt @@ -0,0 +1,98 @@ +# Clip Library +# Copyright (c) 2015-2024 David Capello + +cmake_minimum_required(VERSION 3.5) + +project(clip LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + # Use libc++ explicitly so we can compile for + # CMAKE_OSX_DEPLOYMENT_TARGET=10.7 or 10.8 + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") +endif() + +option(CLIP_ENABLE_IMAGE "Compile with support to copy/paste images" on) +if(WIN32) + option(CLIP_ENABLE_LIST_FORMATS "Compile with support to list clipboard formats" off) +endif() +option(CLIP_EXAMPLES "Compile clip examples" on) +option(CLIP_TESTS "Compile clip tests" on) +if(UNIX AND NOT APPLE) + option(CLIP_X11_WITH_PNG "Compile with libpng to support copy/paste image in png format" on) +endif() + +add_library(clip clip.cpp) + +if(CLIP_ENABLE_IMAGE) + target_sources(clip PRIVATE image.cpp) + target_compile_definitions(clip PUBLIC -DCLIP_ENABLE_IMAGE=1) +endif() + +if(CLIP_ENABLE_LIST_FORMATS) + target_compile_definitions(clip PUBLIC -DCLIP_ENABLE_LIST_FORMATS=1) +endif() + +if(WIN32) + option(CLIP_SUPPORT_WINXP "Enable Windows XP support" OFF) + + target_sources(clip PRIVATE clip_win.cpp) + if(CLIP_ENABLE_IMAGE) + target_sources(clip PRIVATE clip_win_bmp.cpp clip_win_wic.cpp) + target_link_libraries(clip shlwapi) + endif() + + if(MSVC) + target_compile_definitions(clip PRIVATE -D_SCL_SECURE_NO_WARNINGS) + endif() + if (CLIP_SUPPORT_WINXP) + target_compile_definitions(clip PRIVATE -DCLIP_SUPPORT_WINXP) + endif() + + # MinGW requires the windowscodecs just because CLSIDs are defined + # in the windowscodecs.a file instead of the wincodec.h file (?!) + if(MINGW) + find_library(CLIP_WINDOWSCODECS_LIBRARY windowscodecs) + if(CLIP_WINDOWSCODECS_LIBRARY) + target_link_libraries(clip ${CLIP_WINDOWSCODECS_LIBRARY}) + endif() + endif() +elseif(APPLE) + target_compile_options(clip PRIVATE -fobjc-arc) + + find_library(COCOA_LIBRARY Cocoa) + if(COCOA_LIBRARY) + target_sources(clip PRIVATE clip_osx.mm) + target_link_libraries(clip ${COCOA_LIBRARY}) + else() + target_sources(clip PRIVATE clip_none.cpp) + endif() +elseif(UNIX) + include(CheckIncludeFiles) + check_include_files(xcb/xcb.h HAVE_XCB_XLIB_H) + + if(HAVE_XCB_XLIB_H) + target_compile_definitions(clip PRIVATE -DHAVE_XCB_XLIB_H) + target_link_libraries(clip xcb pthread) + + if(CLIP_ENABLE_IMAGE AND CLIP_X11_WITH_PNG) + check_include_files(png.h HAVE_PNG_H) + if(CLIP_X11_PNG_LIBRARY) + set(PNG_LIBRARY ${CLIP_X11_PNG_LIBRARY}) + else() + find_library(PNG_LIBRARY png) + endif() + if(HAVE_PNG_H AND PNG_LIBRARY) + target_compile_definitions(clip PRIVATE -DHAVE_PNG_H) + endif() + target_link_libraries(clip ${PNG_LIBRARY}) + endif() + target_sources(clip PRIVATE clip_x11.cpp) + else() + target_sources(clip PRIVATE clip_none.cpp) + endif() +else() + target_sources(clip PRIVATE clip_none.cpp) +endif() \ No newline at end of file diff --git a/clip-lib/CONTRIBUTING.md b/clip-lib/CONTRIBUTING.md new file mode 100644 index 0000000..6fb6b3d --- /dev/null +++ b/clip-lib/CONTRIBUTING.md @@ -0,0 +1,5 @@ +By submitting a pull request, you represent that you have the right to +license your contribution to the Clip project owners and the community, +agree by submitting the patch that your contributions are licensed under +the [Clip license](https://raw.githubusercontent.com/dacap/clip/main/LICENSE.txt), +and agree to future changes to the licensing. diff --git a/clip-lib/README.md b/clip-lib/README.md new file mode 100644 index 0000000..8e6b12f --- /dev/null +++ b/clip-lib/README.md @@ -0,0 +1,77 @@ +# Clip Library +*Copyright (c) 2015-2024 David Capello* + +[![build](https://github.com/dacap/clip/workflows/build/badge.svg)](https://github.com/dacap/clip/actions?query=workflow%3Abuild) +[![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt) + +Library to copy/retrieve content to/from the clipboard/pasteboard. + +## Features + +Available features on Windows, macOS, and Linux (X11): + +* Copy/paste UTF-8 text. +* Copy/paste user-defined data. +* Copy/paste RGB/RGBA images. This library use non-premultiplied alpha RGB values. + +## Example + +```cpp +#include "clip.h" +#include + +int main() { + clip::set_text("Hello World"); + + std::string value; + clip::get_text(value); + std::cout << value << "\n"; +} +``` + +## User-defined clipboard formats + +```cpp +#include "clip.h" + +int main() { + clip::format my_format = + clip::register_format("com.appname.FormatName"); + + int value = 32; + + clip::lock l; + l.clear(); + l.set_data(clip::text_format(), "Alternative text for value 32"); + l.set_data(my_format, &value, sizeof(int)); +} +``` + +## Platform specific details + +* If two versions of your application (32-bit and 64-bit) can run at + at the same time, remember to avoid storing data types that could + change depending on the platform (e.g. `size_t`) in your custom + format data. +* **Windows**: + - [Limited number of clipboard formats on Windows](http://blogs.msdn.com/b/oldnewthing/archive/2015/03/19/10601208.aspx) +* **Linux**: + - To be able to copy/paste on Linux you need `libx11-dev`/`libX11-devel` package. + - To copy/paste images you will need `libpng-dev`/`libpng-devel` package. + +## Compilation Flags + +* `CLIP_ENABLE_IMAGE`: Enables the support to + [copy](examples/put_image.cpp)/[paste](examples/show_image.cpp) images. +* `CLIP_ENABLE_LIST_FORMATS` (only for Windows): Enables the + `clip::lock::list_formats()` API function and the + [list_clip_formats](examples/list_clip_formats.cpp) example. +* `CLIP_EXAMPLES`: Compile [examples](examples/). +* `CLIP_TESTS`: Compile [tests](tests/). +* `CLIP_X11_WITH_PNG` (only for Linux/X11): Enables support to + copy/paste images using the `libpng` library on Linux. + +## Who is using this library? + +[Check the wiki](https://github.com/dacap/clip/wiki#who-is-using-clip) +to know what projects are using the `clip` library. diff --git a/clip-lib/clip.cpp b/clip-lib/clip.cpp new file mode 100644 index 0000000..16ead84 --- /dev/null +++ b/clip-lib/clip.cpp @@ -0,0 +1,192 @@ +// Clip Library +// Copyright (c) 2015-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" +#include "clip_lock_impl.h" + +#include +#include + +namespace clip { + +namespace { + +void default_error_handler(ErrorCode code) { + static const char* err[] = { + "Cannot lock clipboard", + "Image format is not supported" + }; + throw std::runtime_error(err[static_cast(code)]); +} + +} // anonymous namespace + +error_handler g_error_handler = default_error_handler; + +lock::lock(void* native_window_handle) + : p(new impl(native_window_handle)) { +} + +lock::~lock() = default; + +bool lock::locked() const { + return p->locked(); +} + +bool lock::clear() { + return p->clear(); +} + +bool lock::is_convertible(format f) const { + return p->is_convertible(f); +} + +bool lock::set_data(format f, const char* buf, size_t length) { + return p->set_data(f, buf, length); +} + +bool lock::get_data(format f, char* buf, size_t len) const { + return p->get_data(f, buf, len); +} + +size_t lock::get_data_length(format f) const { + return p->get_data_length(f); +} + +#if CLIP_ENABLE_IMAGE + +bool lock::set_image(const image& img) { + return p->set_image(img); +} + +bool lock::get_image(image& img) const { + return p->get_image(img); +} + +bool lock::get_image_spec(image_spec& spec) const { + return p->get_image_spec(spec); +} + +#endif // CLIP_ENABLE_IMAGE + +#if CLIP_ENABLE_LIST_FORMATS + +std::vector lock::list_formats() const { + return p->list_formats(); +} + +#endif // CLIP_ENABLE_LIST_FORMATS + +format empty_format() { return 0; } +format text_format() { return 1; } +#if CLIP_ENABLE_IMAGE +format image_format() { return 2; } +#endif + +bool has(format f) { + lock l; + if (l.locked()) + return l.is_convertible(f); + else + return false; +} + +bool clear() { + lock l; + if (l.locked()) + return l.clear(); + else + return false; +} + +bool set_text(const std::string& value) { + lock l; + if (l.locked()) { + l.clear(); + return l.set_data(text_format(), value.c_str(), value.size()); + } + else + return false; +} + +bool get_text(std::string& value) { + lock l; + if (!l.locked()) + return false; + + format f = text_format(); + if (!l.is_convertible(f)) + return false; + + size_t len = l.get_data_length(f); + if (len > 0) { + std::vector buf(len); + l.get_data(f, &buf[0], len); + value = &buf[0]; + return true; + } + else { + value.clear(); + return true; + } +} + +#if CLIP_ENABLE_IMAGE + +bool set_image(const image& img) { + lock l; + if (l.locked()) { + l.clear(); + return l.set_image(img); + } + else + return false; +} + +bool get_image(image& img) { + lock l; + if (!l.locked()) + return false; + + format f = image_format(); + if (!l.is_convertible(f)) + return false; + + return l.get_image(img); +} + +bool get_image_spec(image_spec& spec) { + lock l; + if (!l.locked()) + return false; + + format f = image_format(); + if (!l.is_convertible(f)) + return false; + + return l.get_image_spec(spec); +} + +#endif // CLIP_ENABLE_IMAGE + +void set_error_handler(error_handler handler) { + g_error_handler = handler; +} + +error_handler get_error_handler() { + return g_error_handler; +} + +#ifdef HAVE_XCB_XLIB_H +static int g_x11_timeout = 1000; +void set_x11_wait_timeout(int msecs) { g_x11_timeout = msecs; } +int get_x11_wait_timeout() { return g_x11_timeout; } +#else +void set_x11_wait_timeout(int) { } +int get_x11_wait_timeout() { return 1000; } +#endif + +} // namespace clip diff --git a/clip-lib/clip.h b/clip-lib/clip.h new file mode 100644 index 0000000..a6f0913 --- /dev/null +++ b/clip-lib/clip.h @@ -0,0 +1,211 @@ +// Clip Library +// Copyright (c) 2015-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_H_INCLUDED +#define CLIP_H_INCLUDED +#pragma once + +#include +#include +#include +#include + +namespace clip { + + // ====================================================================== + // Low-level API to lock the clipboard/pasteboard and modify it + // ====================================================================== + + // Clipboard format identifier. + typedef size_t format; + +#if CLIP_ENABLE_IMAGE + class image; + struct image_spec; +#endif // CLIP_ENABLE_IMAGE + +#if CLIP_ENABLE_LIST_FORMATS + struct format_info { + format id = 0; + std::string name; + format_info(const format id, + const std::string& name) + : id(id), + name(name) { + } + }; +#endif // CLIP_ENABLE_LIST_FORMATS + + class lock { + public: + // You can give your current HWND as the "native_window_handle." + // Windows clipboard functions use this handle to open/close + // (lock/unlock) the clipboard. From the MSDN documentation we + // need this handler so SetClipboardData() doesn't fail after a + // EmptyClipboard() call. Anyway it looks to work just fine if we + // call OpenClipboard() with a null HWND. + lock(void* native_window_handle = nullptr); + ~lock(); + + // Returns true if we've locked the clipboard successfully in + // lock() constructor. + bool locked() const; + + // Clears the clipboard content. If you don't clear the content, + // previous clipboard content (in unknown formats) could persist + // after the unlock. + bool clear(); + + // Returns true if the clipboard can be converted to the given + // format. + bool is_convertible(format f) const; + bool set_data(format f, const char* buf, size_t len); + bool get_data(format f, char* buf, size_t len) const; + size_t get_data_length(format f) const; + +#if CLIP_ENABLE_IMAGE + // For images + bool set_image(const image& image); + bool get_image(image& image) const; + bool get_image_spec(image_spec& spec) const; +#endif // CLIP_ENABLE_IMAGE + +#if CLIP_ENABLE_LIST_FORMATS + // Returns the list of available formats (by name) in the + // clipboard. + std::vector list_formats() const; +#endif // CLIP_ENABLE_LIST_FORMATS + + private: + class impl; + std::unique_ptr p; + }; + + format register_format(const std::string& name); + + // This format is when the clipboard has no content. + format empty_format(); + + // When the clipboard has UTF8 text. + format text_format(); + +#if CLIP_ENABLE_IMAGE + // When the clipboard has an image. + format image_format(); +#endif + + // Returns true if the clipboard has content of the given type. + bool has(format f); + + // Clears the clipboard content. + bool clear(); + + // ====================================================================== + // Error handling + // ====================================================================== + + enum class ErrorCode { + CannotLock, +#if CLIP_ENABLE_IMAGE + ImageNotSupported, +#endif + }; + + typedef void (*error_handler)(ErrorCode code); + + void set_error_handler(error_handler f); + error_handler get_error_handler(); + + // ====================================================================== + // Text + // ====================================================================== + + // High-level API to put/get UTF8 text in/from the clipboard. These + // functions returns false in case of error. + bool set_text(const std::string& value); + bool get_text(std::string& value); + + // ====================================================================== + // Image + // ====================================================================== + +#if CLIP_ENABLE_IMAGE + + struct image_spec { + unsigned long width = 0; + unsigned long height = 0; + unsigned long bits_per_pixel = 0; + unsigned long bytes_per_row = 0; + unsigned long red_mask = 0; + unsigned long green_mask = 0; + unsigned long blue_mask = 0; + unsigned long alpha_mask = 0; + unsigned long red_shift = 0; + unsigned long green_shift = 0; + unsigned long blue_shift = 0; + unsigned long alpha_shift = 0; + + unsigned long required_data_size() const; + }; + + // The image data must contain straight RGB values + // (non-premultiplied by alpha). The image retrieved from the + // clipboard will be non-premultiplied too. Basically you will be + // always dealing with straight alpha images. + // + // Details: Windows expects premultiplied images on its clipboard + // content, so the library code make the proper conversion + // automatically. macOS handles straight alpha directly, so there is + // no conversion at all. Linux/X11 images are transferred in + // image/png format which are specified in straight alpha. + class image { + public: + image(); + image(const image_spec& spec); + image(const void* data, const image_spec& spec); + image(const image& image); + image(image&& image); + ~image(); + + image& operator=(const image& image); + image& operator=(image&& image); + + char* data() const { return m_data; } + const image_spec& spec() const { return m_spec; } + + bool is_valid() const { return m_data != nullptr; } + void reset(); + + private: + void copy_image(const image& image); + void move_image(image&& image); + + bool m_own_data; + char* m_data; + image_spec m_spec; + }; + + // High-level API to set/get an image in/from the clipboard. These + // functions returns false in case of error. + bool set_image(const image& img); + bool get_image(image& img); + bool get_image_spec(image_spec& spec); + +#endif // CLIP_ENABLE_IMAGE + + // ====================================================================== + // Platform-specific + // ====================================================================== + + // Only for X11: Sets the time (in milliseconds) that we must wait + // for the selection/clipboard owner to receive the content. This + // value is 1000 (one second) by default. + void set_x11_wait_timeout(int msecs); + int get_x11_wait_timeout(); + +} // namespace clip + +#endif // CLIP_H_INCLUDED diff --git a/clip-lib/clip_common.h b/clip-lib/clip_common.h new file mode 100644 index 0000000..61fa992 --- /dev/null +++ b/clip-lib/clip_common.h @@ -0,0 +1,82 @@ +// Clip Library +// Copyright (C) 2020-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_COMMON_H_INCLUDED +#define CLIP_COMMON_H_INCLUDED +#pragma once + +#include "clip.h" + +namespace clip { +namespace details { + +#if CLIP_ENABLE_IMAGE + +inline void divide_rgb_by_alpha(image& img, + bool hasAlphaGreaterThanZero = false) { + const image_spec& spec = img.spec(); + + bool hasValidPremultipliedAlpha = true; + + for (unsigned long y=0; y> spec.red_shift ); + const int g = ((c & spec.green_mask) >> spec.green_shift); + const int b = ((c & spec.blue_mask ) >> spec.blue_shift ); + const int a = ((c & spec.alpha_mask) >> spec.alpha_shift); + + if (a > 0) + hasAlphaGreaterThanZero = true; + if (r > a || g > a || b > a) + hasValidPremultipliedAlpha = false; + } + } + + for (unsigned long y=0; y> spec.red_shift ); + int g = ((c & spec.green_mask) >> spec.green_shift); + int b = ((c & spec.blue_mask ) >> spec.blue_shift ); + int a = ((c & spec.alpha_mask) >> spec.alpha_shift); + + // If all alpha values = 0, we make the image opaque. + if (!hasAlphaGreaterThanZero) { + a = 255; + + // We cannot change the image spec (e.g. spec.alpha_mask=0) to + // make the image opaque, because the "spec" of the image is + // read-only. The image spec used by the client is the one + // returned by get_image_spec(). + } + // If there is alpha information and it's pre-multiplied alpha + else if (hasValidPremultipliedAlpha) { + if (a > 0) { + // Convert it to straight alpha + r = r * 255 / a; + g = g * 255 / a; + b = b * 255 / a; + } + } + + *dst = + (r << spec.red_shift ) | + (g << spec.green_shift) | + (b << spec.blue_shift ) | + (a << spec.alpha_shift); + } + } +} + +#endif // CLIP_ENABLE_IMAGE + +} // namespace details +} // namespace clip + +#endif // CLIP_H_INCLUDED diff --git a/clip-lib/clip_lock_impl.h b/clip-lib/clip_lock_impl.h new file mode 100644 index 0000000..0c678e5 --- /dev/null +++ b/clip-lib/clip_lock_impl.h @@ -0,0 +1,40 @@ +// Clip Library +// Copyright (c) 2015-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_LOCK_IMPL_H_INCLUDED +#define CLIP_LOCK_IMPL_H_INCLUDED + +namespace clip { + +class lock::impl { +public: + impl(void* native_window_handle); + ~impl(); + + bool locked() const { return m_locked; } + bool clear(); + bool is_convertible(format f) const; + bool set_data(format f, const char* buf, size_t len); + bool get_data(format f, char* buf, size_t len) const; + size_t get_data_length(format f) const; + +#if CLIP_ENABLE_IMAGE + bool set_image(const image& image); + bool get_image(image& image) const; + bool get_image_spec(image_spec& spec) const; +#endif // CLIP_ENABLE_IMAGE + +#if CLIP_ENABLE_LIST_FORMATS + std::vector list_formats() const; +#endif // CLIP_ENABLE_LIST_FORMATS + +private: + bool m_locked; +}; + +} // namespace clip + +#endif diff --git a/clip-lib/clip_none.cpp b/clip-lib/clip_none.cpp new file mode 100644 index 0000000..f4cc40c --- /dev/null +++ b/clip-lib/clip_none.cpp @@ -0,0 +1,86 @@ +// Clip Library +// Copyright (c) 2015-2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" +#include "clip_lock_impl.h" + +#include +#include +#include + +namespace clip { + +typedef std::vector Buffer; +typedef std::map Map; + +static format g_last_format = 100; // TODO create an enum with common formats +static Map g_data; + +lock::impl::impl(void* native_handle) : m_locked(true) { +} + +lock::impl::~impl() { +} + +bool lock::impl::clear() { + g_data.clear(); + return true; +} + +bool lock::impl::is_convertible(format f) const { + return (g_data.find(f) != g_data.end()); +} + +bool lock::impl::set_data(format f, const char* buf, size_t len) { + Buffer& dst = g_data[f]; + + dst.resize(len); + if (buf && len > 0) + std::copy(buf, buf+len, dst.begin()); + + if (f == text_format() && + len > 0 && dst.back() != 0) { + dst.push_back(0); + } + + return true; +} + +bool lock::impl::get_data(format f, char* buf, size_t len) const { + assert(buf); + + if (!buf || !is_convertible(f)) + return false; + + const Buffer& src = g_data[f]; + std::copy(src.begin(), src.end(), buf); + return true; +} + +size_t lock::impl::get_data_length(format f) const { + if (is_convertible(f)) + return g_data[f].size(); + else + return 0; +} + +bool lock::impl::set_image(const image& image) { + return false; // TODO +} + +bool lock::impl::get_image(image& image) const { + return false; // TODO +} + +bool lock::impl::get_image_spec(image_spec& spec) const { + return false; // TODO +} + +format register_format(const std::string& name) { + return g_last_format++; +} + +} // namespace clip diff --git a/clip-lib/clip_osx.h b/clip-lib/clip_osx.h new file mode 100644 index 0000000..7346976 --- /dev/null +++ b/clip-lib/clip_osx.h @@ -0,0 +1,35 @@ +// Clip Library +// Copyright (c) 2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_OSX_H_INCLUDED +#define CLIP_OSX_H_INCLUDED +#pragma once + +#ifdef __OBJC__ + +#include + +namespace clip { + +class image; +struct image_spec; + +namespace osx { + +#if CLIP_ENABLE_IMAGE + +bool get_image_from_clipboard(NSPasteboard* pasteboard, + image* output_img, + image_spec* output_spec); + +#endif // CLIP_ENABLE_IMAGE + +} // namespace osx +} // namespace clip + +#endif + +#endif diff --git a/clip-lib/clip_osx.mm b/clip-lib/clip_osx.mm new file mode 100644 index 0000000..a862145 --- /dev/null +++ b/clip-lib/clip_osx.mm @@ -0,0 +1,377 @@ +// Clip Library +// Copyright (c) 2015-2023 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" +#include "clip_common.h" +#include "clip_lock_impl.h" + +#include +#include +#include + +#include +#include +#include + +namespace clip { + +namespace { + + format g_last_format = 100; + std::map g_name_to_format; + std::map g_format_to_name; + +} + +namespace osx { + +#if CLIP_ENABLE_IMAGE + + bool get_image_from_clipboard(NSPasteboard* pasteboard, + image* output_img, + image_spec* output_spec) + { + NSString* result = [pasteboard availableTypeFromArray: + [NSArray arrayWithObjects:NSPasteboardTypeTIFF,NSPasteboardTypePNG,nil]]; + + if (!result) + return false; + + NSData* data = [pasteboard dataForType:result]; + if (!data) + return false; + + NSBitmapImageRep* bitmap = [NSBitmapImageRep imageRepWithData:data]; + + if ((bitmap.bitmapFormat & NSBitmapFormatFloatingPointSamples) || + (bitmap.planar)) { + error_handler e = get_error_handler(); + if (e) + e(ErrorCode::ImageNotSupported); + return false; + } + + image_spec spec; + spec.width = bitmap.pixelsWide; + spec.height = bitmap.pixelsHigh; + spec.bits_per_pixel = bitmap.bitsPerPixel; + spec.bytes_per_row = bitmap.bytesPerRow; + + // We need three samples for Red/Green/Blue + if (bitmap.samplesPerPixel >= 3) { + // Here we are guessing the bits per sample (generally 8, not + // sure how many bits per sample macOS uses for 16bpp + // NSBitmapFormat or if this format is even used). + int bits_per_sample = (bitmap.bitsPerPixel == 16 ? 5: 8); + int bits_shift = 0; + + // With alpha + if (bitmap.alpha) { + if (bitmap.bitmapFormat & NSBitmapFormatAlphaFirst) { + spec.alpha_shift = 0; + bits_shift += bits_per_sample; + } + else { + spec.alpha_shift = 3*bits_per_sample; + } + } + + unsigned long* masks = &spec.red_mask; + unsigned long* shifts = &spec.red_shift; + + // Red/green/blue shifts + for (unsigned long* shift=shifts; shift= 3 && + !(bitmap.bitmapFormat & NSBitmapFormatAlphaNonpremultiplied)) { + details::divide_rgb_by_alpha( + img, + true); // hasAlphaGreaterThanZero=true because we have valid alpha information + } + + std::swap(*output_img, img); + } + + return true; + } + +#endif // CLIP_ENABLE_IMAGE + +} // namespace osx + +lock::impl::impl(void*) : m_locked(true) { +} + +lock::impl::~impl() { +} + +bool lock::impl::clear() { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + return true; + } +} + +bool lock::impl::is_convertible(format f) const { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + NSString* result = nil; + + if (f == text_format()) { + result = [pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSPasteboardTypeString]]; + } +#if CLIP_ENABLE_IMAGE + else if (f == image_format()) { + result = [pasteboard availableTypeFromArray: + [NSArray arrayWithObjects:NSPasteboardTypeTIFF,NSPasteboardTypePNG,nil]]; + } +#endif // CLIP_ENABLE_IMAGE + else { + auto it = g_format_to_name.find(f); + if (it != g_format_to_name.end()) { + const std::string& name = it->second; + NSString* string = [[NSString alloc] initWithBytesNoCopy:(void*)name.c_str() + length:name.size() + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + result = [pasteboard availableTypeFromArray:[NSArray arrayWithObject:string]]; + } + } + + return (result ? true: false); + } +} + +bool lock::impl::set_data(format f, const char* buf, size_t len) { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + + if (f == text_format()) { + NSString* string = [[NSString alloc] initWithBytesNoCopy:(void*)buf + length:len + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + [pasteboard setString:string forType:NSPasteboardTypeString]; + return true; + } + else { + auto it = g_format_to_name.find(f); + if (it != g_format_to_name.end()) { + const std::string& formatName = it->second; + NSString* typeString = [[NSString alloc] + initWithBytesNoCopy:(void*)formatName.c_str() + length:formatName.size() + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + NSData* data = [NSData dataWithBytesNoCopy:(void*)buf + length:len + freeWhenDone:NO]; + + if ([pasteboard setData:data forType:typeString]) + return true; + } + } + return false; + } +} + +bool lock::impl::get_data(format f, char* buf, size_t len) const { + @autoreleasepool { + assert(buf); + if (!buf || !is_convertible(f)) + return false; + + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + + if (f == text_format()) { + NSString* string = [pasteboard stringForType:NSPasteboardTypeString]; + int reqsize = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]+1; + + assert(reqsize <= len); + if (reqsize > len) { + // Buffer is too small + return false; + } + + if (reqsize == 0) + return true; + + memcpy(buf, [string UTF8String], reqsize); + return true; + } + + auto it = g_format_to_name.find(f); + if (it == g_format_to_name.end()) + return false; + + const std::string& formatName = it->second; + NSString* typeString = + [[NSString alloc] initWithBytesNoCopy:(void*)formatName.c_str() + length:formatName.size() + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSData* data = [pasteboard dataForType:typeString]; + if (!data) + return false; + + [data getBytes:buf length:len]; + return true; + } +} + +size_t lock::impl::get_data_length(format f) const { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + + if (f == text_format()) { + NSString* string = [pasteboard stringForType:NSPasteboardTypeString]; + return [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]+1; + } + + auto it = g_format_to_name.find(f); + if (it == g_format_to_name.end()) + return 0; + + const std::string& formatName = it->second; + NSString* typeString = + [[NSString alloc] initWithBytesNoCopy:(void*)formatName.c_str() + length:formatName.size() + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSData* data = [pasteboard dataForType:typeString]; + if (!data) + return 0; + + return data.length; + } +} + +#if CLIP_ENABLE_IMAGE + +bool lock::impl::set_image(const image& image) { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + const image_spec& spec = image.spec(); + + NSBitmapFormat bitmapFormat = 0; + int samples_per_pixel = 0; + if (spec.alpha_mask) { + samples_per_pixel = 4; + if (spec.alpha_shift == 0) + bitmapFormat |= NSBitmapFormatAlphaFirst; + bitmapFormat |= NSBitmapFormatAlphaNonpremultiplied; + } + else if (spec.red_mask || spec.green_mask || spec.blue_mask) { + samples_per_pixel = 3; + } + else { + samples_per_pixel = 1; + } + + if (spec.bits_per_pixel == 32) + bitmapFormat |= NSBitmapFormatThirtyTwoBitLittleEndian; + else if (spec.bits_per_pixel == 16) + bitmapFormat |= NSBitmapFormatSixteenBitLittleEndian; + + std::vector planes(1); + planes[0] = (unsigned char*)image.data(); + + NSBitmapImageRep* bitmap = + [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:&planes[0] + pixelsWide:spec.width + pixelsHigh:spec.height + bitsPerSample:spec.bits_per_pixel / samples_per_pixel + samplesPerPixel:samples_per_pixel + hasAlpha:(spec.alpha_mask ? YES: NO) + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bitmapFormat:bitmapFormat + bytesPerRow:spec.bytes_per_row + bitsPerPixel:spec.bits_per_pixel]; + if (!bitmap) + return false; + + NSData* data = bitmap.TIFFRepresentation; + if (!data) + return false; + + if ([pasteboard setData:data forType:NSPasteboardTypeTIFF]) + return true; + + return false; + } +} + +bool lock::impl::get_image(image& img) const { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + return osx::get_image_from_clipboard(pasteboard, &img, nullptr); +} + +bool lock::impl::get_image_spec(image_spec& spec) const { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + return osx::get_image_from_clipboard(pasteboard, nullptr, &spec); +} + +#endif // CLIP_ENABLE_IMAGE + +format register_format(const std::string& name) { + // Check if the format is already registered + auto it = g_name_to_format.find(name); + if (it != g_name_to_format.end()) + return it->second; + + format new_format = g_last_format++; + g_name_to_format[name] = new_format; + g_format_to_name[new_format] = name; + return new_format; +} + +} // namespace clip diff --git a/clip-lib/clip_win.cpp b/clip-lib/clip_win.cpp new file mode 100644 index 0000000..058ea43 --- /dev/null +++ b/clip-lib/clip_win.cpp @@ -0,0 +1,407 @@ +// Clip Library +// Copyright (C) 2015-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip_win.h" + +#include "clip.h" +#include "clip_lock_impl.h" + +#include +#include +#include +#include +#include + +namespace clip { + +namespace { + +// Data type used as header for custom formats to indicate the exact +// size of the user custom data. This is necessary because it looks +// like GlobalSize() might not return the exact size, but a greater +// value. +typedef uint64_t CustomSizeT; + +class Hglobal { +public: + Hglobal() : m_handle(nullptr) { + } + + explicit Hglobal(HGLOBAL handle) : m_handle(handle) { + } + + explicit Hglobal(size_t len) : m_handle(GlobalAlloc(GHND, len)) { + } + + ~Hglobal() { + if (m_handle) + GlobalFree(m_handle); + } + + void release() { + m_handle = nullptr; + } + + operator HGLOBAL() { + return m_handle; + } + +private: + HGLOBAL m_handle; +}; + +// From: https://issues.chromium.org/issues/40080988#comment8 +// +// "Adds impersonation of the anonymous token around calls to the +// CloseClipboard() system call. On Windows 8+ the win32k driver +// captures the access token of the caller and makes it available to +// other users on the desktop through the system call +// GetClipboardAccessToken(). This introduces a risk of privilege +// escalation in sandboxed processes. By performing the +// impersonation then whenever Chrome writes data to the clipboard +// only the anonymous token is available." +// +class AnonymousTokenImpersonator { +public: + AnonymousTokenImpersonator() + : m_must_revert(ImpersonateAnonymousToken(GetCurrentThread())) + {} + + ~AnonymousTokenImpersonator() { + if (m_must_revert) + RevertToSelf(); + } +private: + const bool m_must_revert; +}; + +} // anonymous namespace + +lock::impl::impl(void* hwnd) : m_locked(false) { + for (int i=0; i<5; ++i) { + if (OpenClipboard((HWND)hwnd)) { + m_locked = true; + break; + } + Sleep(20); + } + + if (!m_locked) { + error_handler e = get_error_handler(); + if (e) + e(ErrorCode::CannotLock); + } +} + +lock::impl::~impl() { + if (m_locked) { + AnonymousTokenImpersonator guard; + CloseClipboard(); + } +} + +bool lock::impl::clear() { + return (EmptyClipboard() ? true: false); +} + +bool lock::impl::is_convertible(format f) const { + if (f == text_format()) { + return + (IsClipboardFormatAvailable(CF_TEXT) || + IsClipboardFormatAvailable(CF_UNICODETEXT) || + IsClipboardFormatAvailable(CF_OEMTEXT)); + } +#if CLIP_ENABLE_IMAGE + else if (f == image_format()) { + return (IsClipboardFormatAvailable(CF_DIB) || + win::wic_image_format_available(nullptr) != nullptr); + } +#endif // CLIP_ENABLE_IMAGE + else + return IsClipboardFormatAvailable(f); +} + +bool lock::impl::set_data(format f, const char* buf, size_t len) { + bool result = false; + + if (f == text_format()) { + if (len > 0) { + int reqsize = MultiByteToWideChar(CP_UTF8, 0, buf, len, NULL, 0); + if (reqsize > 0) { + ++reqsize; + + Hglobal hglobal(sizeof(WCHAR)*reqsize); + LPWSTR lpstr = static_cast(GlobalLock(hglobal)); + MultiByteToWideChar(CP_UTF8, 0, buf, len, lpstr, reqsize); + GlobalUnlock(hglobal); + + result = (SetClipboardData(CF_UNICODETEXT, hglobal)) ? true: false; + if (result) + hglobal.release(); + } + } + } + else { + Hglobal hglobal(len+sizeof(CustomSizeT)); + if (hglobal) { + auto dst = (uint8_t*)GlobalLock(hglobal); + if (dst) { + *((CustomSizeT*)dst) = len; + memcpy(dst+sizeof(CustomSizeT), buf, len); + GlobalUnlock(hglobal); + result = (SetClipboardData(f, hglobal) ? true: false); + if (result) + hglobal.release(); + } + } + } + + return result; +} + +bool lock::impl::get_data(format f, char* buf, size_t len) const { + assert(buf); + + if (!buf || !is_convertible(f)) + return false; + + bool result = false; + + if (f == text_format()) { + if (IsClipboardFormatAvailable(CF_UNICODETEXT)) { + HGLOBAL hglobal = GetClipboardData(CF_UNICODETEXT); + if (hglobal) { + LPWSTR lpstr = static_cast(GlobalLock(hglobal)); + if (lpstr) { + size_t reqsize = + WideCharToMultiByte(CP_UTF8, 0, lpstr, -1, + nullptr, 0, nullptr, nullptr); + + assert(reqsize <= len); + if (reqsize <= len) { + WideCharToMultiByte(CP_UTF8, 0, lpstr, -1, + buf, reqsize, nullptr, nullptr); + result = true; + } + GlobalUnlock(hglobal); + } + } + } + else if (IsClipboardFormatAvailable(CF_TEXT)) { + HGLOBAL hglobal = GetClipboardData(CF_TEXT); + if (hglobal) { + LPSTR lpstr = static_cast(GlobalLock(hglobal)); + if (lpstr) { + // TODO check length + memcpy(buf, lpstr, len); + result = true; + GlobalUnlock(hglobal); + } + } + } + } + else { + if (IsClipboardFormatAvailable(f)) { + HGLOBAL hglobal = GetClipboardData(f); + if (hglobal) { + const SIZE_T total_size = GlobalSize(hglobal); + auto ptr = (const uint8_t*)GlobalLock(hglobal); + if (ptr) { + CustomSizeT reqsize = *((CustomSizeT*)ptr); + + // If the registered length of data in the first CustomSizeT + // number of bytes of the hglobal data is greater than the + // GlobalSize(hglobal), something is wrong, it should not + // happen. + assert(reqsize <= total_size); + if (reqsize > total_size) + reqsize = total_size - sizeof(CustomSizeT); + + if (reqsize <= len) { + memcpy(buf, ptr+sizeof(CustomSizeT), reqsize); + result = true; + } + GlobalUnlock(hglobal); + } + } + } + } + + return result; +} + +size_t lock::impl::get_data_length(format f) const { + size_t len = 0; + + if (f == text_format()) { + if (IsClipboardFormatAvailable(CF_UNICODETEXT)) { + HGLOBAL hglobal = GetClipboardData(CF_UNICODETEXT); + if (hglobal) { + LPWSTR lpstr = static_cast(GlobalLock(hglobal)); + if (lpstr) { + len = + WideCharToMultiByte(CP_UTF8, 0, lpstr, -1, + nullptr, 0, nullptr, nullptr); + GlobalUnlock(hglobal); + } + } + } + else if (IsClipboardFormatAvailable(CF_TEXT)) { + HGLOBAL hglobal = GetClipboardData(CF_TEXT); + if (hglobal) { + LPSTR lpstr = (LPSTR)GlobalLock(hglobal); + if (lpstr) { + len = strlen(lpstr) + 1; + GlobalUnlock(hglobal); + } + } + } + } + else if (f != empty_format()) { + if (IsClipboardFormatAvailable(f)) { + HGLOBAL hglobal = GetClipboardData(f); + if (hglobal) { + const SIZE_T total_size = GlobalSize(hglobal); + auto ptr = (const uint8_t*)GlobalLock(hglobal); + if (ptr) { + len = *((CustomSizeT*)ptr); + + assert(len <= total_size); + if (len > total_size) + len = total_size - sizeof(CustomSizeT); + + GlobalUnlock(hglobal); + } + } + } + } + + return len; +} + +#if CLIP_ENABLE_LIST_FORMATS + +std::vector lock::impl::list_formats() const { + static const char* standard_formats[CF_MAX] = { + "", "CF_TEXT", "CF_BITMAP", "CF_METAFILEPICT", + "CF_SYLK", "CF_DIF", "CF_TIFF", "CF_OEMTEXT", + "CF_DIB", "CF_PALETTE", "CF_PENDATA", "CF_RIFF", + "CF_WAVE", "CF_UNICODETEXT", "CF_ENHMETAFILE", "CF_HDROP", + "CF_LOCALE", "CF_DIBV5" + }; + + std::vector formats; + std::vector format_name(512); + + formats.reserve(CountClipboardFormats()); + + UINT format_id = EnumClipboardFormats(0); + while (format_id != 0) { + if (format_id >= CF_TEXT && format_id < CF_MAX) { + // Standard clipboard format + formats.emplace_back(format_id, standard_formats[format_id]); + } + // Get user-defined format name + else { + int size = GetClipboardFormatNameA( + format_id, + format_name.data(), + format_name.size()); + + formats.emplace_back(format_id, std::string(format_name.data(), size)); + } + + format_id = EnumClipboardFormats(format_id); + } + + return formats; +} + +#endif // CLIP_ENABLE_LIST_FORMATS + +#if CLIP_ENABLE_IMAGE + +bool lock::impl::set_image(const image& image) { + const image_spec& spec = image.spec(); + + // Add the PNG clipboard format for images with alpha channel + // (useful to communicate with some Windows programs that only use + // alpha data from PNG clipboard format) + if (spec.bits_per_pixel == 32 && + spec.alpha_mask) { + UINT png_format = RegisterClipboardFormatA("PNG"); + if (png_format) { + Hglobal png_handle(win::write_png(image)); + if (png_handle) + SetClipboardData(png_format, png_handle); + } + } + + Hglobal hmem(clip::win::create_dibv5(image)); + if (!hmem) + return false; + + SetClipboardData(CF_DIBV5, hmem); + return true; +} + +bool lock::impl::get_image(image& output_img) const { + // Tries to get the first image format that can be read using WIC + // ("PNG", "JPG", "GIF", etc). + UINT cbformat; + if (auto read_img = win::wic_image_format_available(&cbformat)) { + HANDLE handle = GetClipboardData(cbformat); + if (handle) { + size_t size = GlobalSize(handle); + uint8_t* data = (uint8_t*)GlobalLock(handle); + bool result = read_img(data, size, &output_img, nullptr); + GlobalUnlock(handle); + if (result) + return true; + } + } + + // If we couldn't find any, we try to use the regular DIB format. + win::BitmapInfo bi; + return bi.to_image(output_img); +} + +bool lock::impl::get_image_spec(image_spec& spec) const { + UINT cbformat; + if (auto read_img = win::wic_image_format_available(&cbformat)) { + HANDLE handle = GetClipboardData(cbformat); + if (handle) { + size_t size = GlobalSize(handle); + uint8_t* data = (uint8_t*)GlobalLock(handle); + bool result = read_img(data, size, nullptr, &spec); + GlobalUnlock(handle); + if (result) + return true; + } + } + + win::BitmapInfo bi; + if (!bi.is_valid()) + return false; + bi.fill_spec(spec); + return true; +} + +#endif // CLIP_ENABLE_IMAGE + +format register_format(const std::string& name) { + int reqsize = 1+MultiByteToWideChar(CP_UTF8, 0, + name.c_str(), name.size(), NULL, 0); + std::vector buf(reqsize); + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), name.size(), + &buf[0], reqsize); + + // From MSDN, registered clipboard formats are identified by values + // in the range 0xC000 through 0xFFFF. + return (format)RegisterClipboardFormatW(&buf[0]); +} + +} // namespace clip diff --git a/clip-lib/clip_win.h b/clip-lib/clip_win.h new file mode 100644 index 0000000..8efccd8 --- /dev/null +++ b/clip-lib/clip_win.h @@ -0,0 +1,26 @@ +// Clip Library +// Copyright (c) 2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_WIN_H_INCLUDED +#define CLIP_WIN_H_INCLUDED +#pragma once + +#include + +#ifndef LCS_WINDOWS_COLOR_SPACE +#define LCS_WINDOWS_COLOR_SPACE 'Win ' +#endif + +#ifndef CF_DIBV5 +#define CF_DIBV5 17 +#endif + +#if CLIP_ENABLE_IMAGE + #include "clip_win_bmp.h" + #include "clip_win_wic.h" +#endif + +#endif // CLIP_WIN_H_INCLUDED diff --git a/clip-lib/clip_win_bmp.cpp b/clip-lib/clip_win_bmp.cpp new file mode 100644 index 0000000..ffb0d6f --- /dev/null +++ b/clip-lib/clip_win_bmp.cpp @@ -0,0 +1,347 @@ +// Clip Library +// Copyright (c) 2015-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip_win_bmp.h" + +#include "clip.h" +#include "clip_common.h" + +#include + +namespace clip { +namespace win { + +namespace { + +unsigned long get_shift_from_mask(unsigned long mask) { + unsigned long shift = 0; + for (shift=0; shiftbV5BitCount == 32 && + ((b5->bV5Compression == BI_RGB) || + (b5->bV5Compression == BI_BITFIELDS && + b5->bV5RedMask && b5->bV5GreenMask && + b5->bV5BlueMask && b5->bV5AlphaMask))) { + width = b5->bV5Width; + height = b5->bV5Height; + bit_count = b5->bV5BitCount; + compression = b5->bV5Compression; + if (compression == BI_BITFIELDS) { + red_mask = b5->bV5RedMask; + green_mask = b5->bV5GreenMask; + blue_mask = b5->bV5BlueMask; + alpha_mask = b5->bV5AlphaMask; + } + else { + red_mask = 0xff0000; + green_mask = 0xff00; + blue_mask = 0xff; + alpha_mask = 0xff000000; + } + return true; + } + return false; +} + +bool BitmapInfo::load_from(BITMAPINFO* bi) { + if (!bi) + return false; + + width = bi->bmiHeader.biWidth; + height = bi->bmiHeader.biHeight; + bit_count = bi->bmiHeader.biBitCount; + compression = bi->bmiHeader.biCompression; + + if (compression == BI_BITFIELDS) { + red_mask = *((uint32_t*)&bi->bmiColors[0]); + green_mask = *((uint32_t*)&bi->bmiColors[1]); + blue_mask = *((uint32_t*)&bi->bmiColors[2]); + if (bit_count == 32) + alpha_mask = 0xff000000; + return true; + } + if (compression == BI_RGB) { + switch (bit_count) { + case 32: + red_mask = 0xff0000; + green_mask = 0xff00; + blue_mask = 0xff; + alpha_mask = 0xff000000; + break; + case 24: + case 8: // We return 8bpp images as 24bpp + red_mask = 0xff0000; + green_mask = 0xff00; + blue_mask = 0xff; + break; + case 16: + red_mask = 0x7c00; + green_mask = 0x03e0; + blue_mask = 0x001f; + break; + } + return true; + } + return false; +} + +BitmapInfo::BitmapInfo(BITMAPV5HEADER* pb5) { + if (load_from(pb5)) + b5 = pb5; +} + +BitmapInfo::BitmapInfo(BITMAPINFO* pbi) { + if (load_from(pbi)) + bi = pbi; +} + +void BitmapInfo::fill_spec(image_spec& spec) const { + spec.width = width; + spec.height = (height >= 0 ? height: -height); + // We convert indexed to 24bpp RGB images to match the OS X behavior + spec.bits_per_pixel = bit_count; + if (spec.bits_per_pixel <= 8) + spec.bits_per_pixel = 24; + spec.bytes_per_row = width*((spec.bits_per_pixel+7)/8); + spec.red_mask = red_mask; + spec.green_mask = green_mask; + spec.blue_mask = blue_mask; + spec.alpha_mask = alpha_mask; + + switch (spec.bits_per_pixel) { + + case 24: { + // We need one extra byte to avoid a crash updating the last + // pixel on last row using: + // + // *((uint32_t*)ptr) = pixel24bpp; + // + ++spec.bytes_per_row; + + // Align each row to 32bpp + int padding = (4-(spec.bytes_per_row&3))&3; + spec.bytes_per_row += padding; + break; + } + + case 16: { + int padding = (4-(spec.bytes_per_row&3))&3; + spec.bytes_per_row += padding; + break; + } + } + + unsigned long* masks = &spec.red_mask; + unsigned long* shifts = &spec.red_shift; + for (unsigned long* shift=shifts, *mask=masks; shiftbV5Size; + else + src = ((uint8_t*)bi) + bi->bmiHeader.biSize; + if (compression == BI_BITFIELDS) + src += sizeof(RGBQUAD)*3; + } + + if (src) { + const int src_bytes_per_row = spec.width*((bit_count+7)/8); + const int padding = (4-(src_bytes_per_row&3))&3; + + for (long y=spec.height-1; y>=0; --y, src+=src_bytes_per_row+padding) { + char* dst = img.data()+y*spec.bytes_per_row; + std::copy(src, src+src_bytes_per_row, dst); + } + } + + // Windows uses premultiplied RGB values, and we use straight + // alpha. So we have to divide all RGB values by its alpha. + if (bit_count == 32 && spec.alpha_mask) { + details::divide_rgb_by_alpha(img); + } + break; + } + + case 8: { + assert(bi); + + const int colors = (bi->bmiHeader.biClrUsed > 0 ? bi->bmiHeader.biClrUsed: 256); + std::vector palette(colors); + for (int c=0; cbmiColors[c].rgbRed << spec.red_shift) | + (bi->bmiColors[c].rgbGreen << spec.green_shift) | + (bi->bmiColors[c].rgbBlue << spec.blue_shift); + } + + const uint8_t* src = (((uint8_t*)bi) + bi->bmiHeader.biSize + sizeof(RGBQUAD)*colors); + const int padding = (4-(spec.width&3))&3; + + for (long y=spec.height-1; y>=0; --y, src+=padding) { + char* dst = img.data()+y*spec.bytes_per_row; + + for (unsigned long x=0; x= colors) + idx = colors-1; + + *((uint32_t*)dst) = palette[idx]; + } + } + break; + } + } + + std::swap(output_img, img); + return true; +} + +HGLOBAL create_dibv5(const image& image) { + const image_spec& spec = image.spec(); + image_spec out_spec = spec; + + int palette_colors = 0; + int padding = 0; + switch (spec.bits_per_pixel) { + case 24: padding = (4-((spec.width*3)&3))&3; break; + case 16: padding = ((4-((spec.width*2)&3))&3)/2; break; + case 8: padding = (4-(spec.width&3))&3; break; + } + out_spec.bytes_per_row += padding; + + // Create the BITMAPV5HEADER structure + HGLOBAL hmem = + GlobalAlloc( + GHND, + sizeof(BITMAPV5HEADER) + + palette_colors*sizeof(RGBQUAD) + + out_spec.bytes_per_row*out_spec.height); + if (!hmem) + return nullptr; + + out_spec.red_mask = 0x00ff0000; + out_spec.green_mask = 0xff00; + out_spec.blue_mask = 0xff; + out_spec.alpha_mask = 0xff000000; + out_spec.red_shift = 16; + out_spec.green_shift = 8; + out_spec.blue_shift = 0; + out_spec.alpha_shift = 24; + + BITMAPV5HEADER* bi = (BITMAPV5HEADER*)GlobalLock(hmem); + bi->bV5Size = sizeof(BITMAPV5HEADER); + bi->bV5Width = out_spec.width; + bi->bV5Height = out_spec.height; + bi->bV5Planes = 1; + bi->bV5BitCount = (WORD)out_spec.bits_per_pixel; + bi->bV5Compression = BI_RGB; + bi->bV5SizeImage = out_spec.bytes_per_row*spec.height; + bi->bV5RedMask = out_spec.red_mask; + bi->bV5GreenMask = out_spec.green_mask; + bi->bV5BlueMask = out_spec.blue_mask; + bi->bV5AlphaMask = out_spec.alpha_mask; + bi->bV5CSType = LCS_WINDOWS_COLOR_SPACE; + bi->bV5Intent = LCS_GM_GRAPHICS; + bi->bV5ClrUsed = 0; + + switch (spec.bits_per_pixel) { + case 32: { + const char* src = image.data(); + char* dst = (((char*)bi)+bi->bV5Size) + (out_spec.height-1)*out_spec.bytes_per_row; + for (long y=spec.height-1; y>=0; --y) { + const uint32_t* src_x = (const uint32_t*)src; + uint32_t* dst_x = (uint32_t*)dst; + + for (unsigned long x=0; x> spec.red_shift ); + int g = ((c & spec.green_mask) >> spec.green_shift); + int b = ((c & spec.blue_mask ) >> spec.blue_shift ); + int a = ((c & spec.alpha_mask) >> spec.alpha_shift); + + // Windows requires premultiplied RGBA values + r = r * a / 255; + g = g * a / 255; + b = b * a / 255; + + *dst_x = + (r << out_spec.red_shift ) | + (g << out_spec.green_shift) | + (b << out_spec.blue_shift ) | + (a << out_spec.alpha_shift); + } + + src += spec.bytes_per_row; + dst -= out_spec.bytes_per_row; + } + break; + } + default: + GlobalUnlock(hmem); + GlobalFree(hmem); + + error_handler e = get_error_handler(); + if (e) + e(ErrorCode::ImageNotSupported); + return nullptr; + } + + GlobalUnlock(hmem); + return hmem; +} + +} // namespace win +} // namespace clip diff --git a/clip-lib/clip_win_bmp.h b/clip-lib/clip_win_bmp.h new file mode 100644 index 0000000..5c9c38b --- /dev/null +++ b/clip-lib/clip_win_bmp.h @@ -0,0 +1,66 @@ +// Clip Library +// Copyright (c) 2015-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_WIN_BMP_H_INCLUDED +#define CLIP_WIN_BMP_H_INCLUDED +#pragma once + +#if !CLIP_ENABLE_IMAGE + #error This file can be include only when CLIP_ENABLE_IMAGE is defined +#endif + +#include + +#include + +namespace clip { + +class image; +struct image_spec; + +namespace win { + +struct BitmapInfo { + BITMAPV5HEADER* b5 = nullptr; + BITMAPINFO* bi = nullptr; + int width = 0; + int height = 0; + uint16_t bit_count = 0; + uint32_t compression = 0; + uint32_t red_mask = 0; + uint32_t green_mask = 0; + uint32_t blue_mask = 0; + uint32_t alpha_mask = 0; + + BitmapInfo(); + explicit BitmapInfo(BITMAPV5HEADER* pb5); + explicit BitmapInfo(BITMAPINFO* pbi); + + bool is_valid() const { + return (b5 || bi); + } + + void fill_spec(image_spec& spec) const; + + // Fills the output_img with the data provided by this + // BitmapInfo. Returns true if it was able to fill the output image + // or false otherwise. + bool to_image(image& output_img) const; + +private: + bool load_from(BITMAPV5HEADER* b5); + bool load_from(BITMAPINFO* bi); +}; + +// Returns a handle to the HGLOBAL memory reserved to create a DIBV5 +// based on the image passed by parameter. Returns null if it cannot +// create the handle. +HGLOBAL create_dibv5(const image& image); + +} // namespace win +} // namespace clip + +#endif // CLIP_WIN_BMP_H_INCLUDED diff --git a/clip-lib/clip_win_wic.cpp b/clip-lib/clip_win_wic.cpp new file mode 100644 index 0000000..dd37158 --- /dev/null +++ b/clip-lib/clip_win_wic.cpp @@ -0,0 +1,472 @@ +// Clip Library +// Copyright (c) 2020-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip_win_wic.h" + +#include "clip.h" + +#include +#include + +#include +#include + +namespace clip { +namespace win { + +namespace { + +// Successful calls to CoInitialize() (S_OK or S_FALSE) must match +// the calls to CoUninitialize(). +// From: https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize#remarks +struct coinit { + HRESULT hr; + coinit() { + hr = CoInitialize(nullptr); + } + ~coinit() { + if (hr == S_OK || hr == S_FALSE) + CoUninitialize(); + } +}; + +template +class comptr { +public: + comptr() { } + explicit comptr(T* ptr) : m_ptr(ptr) { } + comptr(const comptr&) = delete; + comptr& operator=(const comptr&) = delete; + ~comptr() { reset(); } + + T** operator&() { return &m_ptr; } + T* operator->() { return m_ptr; } + bool operator!() const { return !m_ptr; } + + T* get() { return m_ptr; } + void reset() { + if (m_ptr) { + m_ptr->Release(); + m_ptr = nullptr; + } + } +private: + T* m_ptr = nullptr; +}; + +#ifdef CLIP_SUPPORT_WINXP +class hmodule { +public: + hmodule(LPCWSTR name) : m_ptr(LoadLibraryW(name)) { } + hmodule(const hmodule&) = delete; + hmodule& operator=(const hmodule&) = delete; + ~hmodule() { + if (m_ptr) + FreeLibrary(m_ptr); + } + + operator HMODULE() { return m_ptr; } + bool operator!() const { return !m_ptr; } +private: + HMODULE m_ptr = nullptr; +}; +#endif + +struct WicImageFormat { + const char* names[3]; // Alternative names of this format + UINT ids[3]; // Clipboard format ID for each name of this format + ReadWicImageFormatFunc read; // Function used to decode data in this format +}; + +WicImageFormat wic_image_formats[] = { + { { "PNG", "image/png", nullptr }, { 0, 0, 0 }, read_png }, + { { "JPG", "image/jpeg", "JPEG" }, { 0, 0, 0 }, read_jpg }, + { { "BMP", "image/bmp", nullptr }, { 0, 0, 0 }, read_bmp }, + { { "GIF", "image/gif", nullptr }, { 0, 0, 0 }, read_gif } +}; + +} // anonymous namespace + +ReadWicImageFormatFunc wic_image_format_available(UINT* output_cbformat) { + for (auto& fmt : wic_image_formats) { + for (int i=0; i<3; ++i) { + const char* name = fmt.names[i]; + if (!name) + break; + + // Although RegisterClipboardFormatA() already returns the same + // value for the same "name" (even for different apps), we + // prefer to cache the value to avoid calling + // RegisterClipboardFormatA() several times (as internally that + // function must do some kind of hash map name -> ID + // conversion). + UINT cbformat = fmt.ids[i]; + if (cbformat == 0) + fmt.ids[i] = cbformat = RegisterClipboardFormatA(name); + + if (cbformat && IsClipboardFormatAvailable(cbformat)) { + if (output_cbformat) + *output_cbformat = cbformat; + return fmt.read; + } + } + } + return nullptr; +} + +////////////////////////////////////////////////////////////////////// +// Encode the image as PNG format + +bool write_png_on_stream(const image& image, + IStream* stream) { + const image_spec& spec = image.spec(); + + comptr encoder; + HRESULT hr = CoCreateInstance(CLSID_WICPngEncoder, + nullptr, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&encoder)); + if (FAILED(hr)) + return false; + + hr = encoder->Initialize(stream, WICBitmapEncoderNoCache); + if (FAILED(hr)) + return false; + + comptr frame; + comptr options; + hr = encoder->CreateNewFrame(&frame, &options); + if (FAILED(hr)) + return false; + + hr = frame->Initialize(options.get()); + if (FAILED(hr)) + return false; + + // PNG encoder (and decoder) only supports GUID_WICPixelFormat32bppBGRA for 32bpp. + // See: https://docs.microsoft.com/en-us/windows/win32/wic/-wic-codec-native-pixel-formats#png-native-codec + WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA; + hr = frame->SetPixelFormat(&pixelFormat); + if (FAILED(hr)) + return false; + + hr = frame->SetSize(spec.width, spec.height); + if (FAILED(hr)) + return false; + + std::vector buf; + uint8_t* ptr = (uint8_t*)image.data(); + int bytes_per_row = spec.bytes_per_row; + + // Convert to GUID_WICPixelFormat32bppBGRA if needed + if (spec.red_mask != 0xff0000 || + spec.green_mask != 0xff00 || + spec.blue_mask != 0xff || + spec.alpha_mask != 0xff000000) { + buf.resize(spec.width * spec.height); + uint32_t* dst = (uint32_t*)&buf[0]; + uint32_t* src = (uint32_t*)image.data(); + for (int y=0; y> spec.red_shift ) << 16) | + (((c & spec.green_mask) >> spec.green_shift) << 8) | + (((c & spec.blue_mask ) >> spec.blue_shift ) ) | + (((c & spec.alpha_mask) >> spec.alpha_shift) << 24)); + ++dst; + ++src; + } + src = (uint32_t*)(((uint8_t*)src_line_start) + spec.bytes_per_row); + } + ptr = (uint8_t*)&buf[0]; + bytes_per_row = 4 * spec.width; + } + + hr = frame->WritePixels(spec.height, + bytes_per_row, + bytes_per_row * spec.height, + (BYTE*)ptr); + if (FAILED(hr)) + return false; + + hr = frame->Commit(); + if (FAILED(hr)) + return false; + + hr = encoder->Commit(); + if (FAILED(hr)) + return false; + + return true; +} + +HGLOBAL write_png(const image& image) { + coinit com; + + comptr stream; + HRESULT hr = CreateStreamOnHGlobal(nullptr, false, &stream); + if (FAILED(hr)) + return nullptr; + + bool result = write_png_on_stream(image, stream.get()); + + HGLOBAL handle; + hr = GetHGlobalFromStream(stream.get(), &handle); + if (result) + return handle; + + GlobalFree(handle); + return nullptr; +} + +IStream* create_stream(const BYTE* pInit, UINT cbInit) +{ +#ifdef CLIP_SUPPORT_WINXP + // Pull SHCreateMemStream from shlwapi.dll by ordinal 12 + // for Windows XP support + // From: https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-shcreatememstream#remarks + + typedef IStream*(WINAPI * SHCreateMemStreamPtr)(const BYTE* pInit, + UINT cbInit); + hmodule shlwapiDll(L"shlwapi.dll"); + if (!shlwapiDll) + return false; + + auto SHCreateMemStream = reinterpret_cast( + GetProcAddress(shlwapiDll, (LPCSTR)12)); + if (!SHCreateMemStream) + return false; +#endif + return SHCreateMemStream(pInit, cbInit); +} + +image_spec spec_from_pixelformat(const WICPixelFormatGUID& pixelFormat, unsigned long w, unsigned long h) +{ + image_spec spec; + spec.width = w; + spec.height = h; + if (pixelFormat == GUID_WICPixelFormat32bppBGRA || + pixelFormat == GUID_WICPixelFormat32bppBGR) { + spec.bits_per_pixel = 32; + spec.red_mask = 0xff0000; + spec.green_mask = 0xff00; + spec.blue_mask = 0xff; + spec.alpha_mask = 0xff000000; + spec.red_shift = 16; + spec.green_shift = 8; + spec.blue_shift = 0; + spec.alpha_shift = 24; + // Reset mask and shift for BGR pixel format. + if (pixelFormat == GUID_WICPixelFormat32bppBGR) { + spec.alpha_mask = 0; + spec.alpha_shift = 0; + } + } + else if (pixelFormat == GUID_WICPixelFormat24bppBGR || + pixelFormat == GUID_WICPixelFormat8bppIndexed) { + spec.bits_per_pixel = 24; + spec.red_mask = 0xff0000; + spec.green_mask = 0xff00; + spec.blue_mask = 0xff; + spec.alpha_mask = 0; + spec.red_shift = 16; + spec.green_shift = 8; + spec.blue_shift = 0; + spec.alpha_shift = 0; + } + spec.bytes_per_row = ((w*spec.bits_per_pixel+31) / 32) * 4; + return spec; +} + +// Tries to decode the input buf of size len using the specified +// decoders. If output_image is not null, the decoded image is +// returned there, if output_spec is not null then the image +// specifications are set there. +bool decode(const GUID decoder_clsid1, + const GUID decoder_clsid2, + const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec) +{ + coinit com; + + comptr decoder; + HRESULT hr = CoCreateInstance(decoder_clsid1, nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&decoder)); + if (FAILED(hr) && decoder_clsid2 != GUID_NULL) { + hr = CoCreateInstance(decoder_clsid2, nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&decoder)); + } + if (FAILED(hr)) + return false; + + // Can decoder be nullptr if hr is S_OK/successful? We've received + // some crash reports that might indicate this. + if (!decoder) + return false; + + comptr stream(create_stream(buf, len)); + if (!stream) + return false; + + hr = decoder->Initialize(stream.get(), WICDecodeMetadataCacheOnDemand); + if (FAILED(hr)) + return false; + + comptr frame; + hr = decoder->GetFrame(0, &frame); + if (FAILED(hr)) + return false; + + WICPixelFormatGUID pixelFormat; + hr = frame->GetPixelFormat(&pixelFormat); + if (FAILED(hr)) + return false; + + // Only support these pixel formats + // TODO add support for more pixel formats + if (pixelFormat != GUID_WICPixelFormat32bppBGRA && + pixelFormat != GUID_WICPixelFormat32bppBGR && + pixelFormat != GUID_WICPixelFormat24bppBGR && + pixelFormat != GUID_WICPixelFormat8bppIndexed) + return false; + + UINT width = 0, height = 0; + hr = frame->GetSize(&width, &height); + if (FAILED(hr)) + return false; + + image_spec spec = spec_from_pixelformat(pixelFormat, width, height); + + if (output_spec) + *output_spec = spec; + + image img; + if (output_image) { + if (pixelFormat == GUID_WICPixelFormat8bppIndexed) { + std::vector pixels(spec.width * spec.height); + hr = frame->CopyPixels(nullptr, // Entire bitmap + spec.width, + spec.width * spec.height, + pixels.data()); + + if (FAILED(hr)) + return false; + + comptr factory; + HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&factory)); + if (FAILED(hr)) + return false; + + comptr palette; + hr = factory->CreatePalette(&palette); + if (FAILED(hr)) + return false; + + hr = frame->CopyPalette(palette.get()); + if (FAILED(hr)) + return false; + + UINT numcolors; + hr = palette->GetColorCount(&numcolors); + if (FAILED(hr)) + return false; + + UINT actualNumcolors; + std::vector colors(numcolors); + hr = palette->GetColors(numcolors, colors.data(), &actualNumcolors); + if (FAILED(hr)) + return false; + + BOOL hasAlpha = false; + palette->HasAlpha(&hasAlpha); + if (hasAlpha) { + spec = spec_from_pixelformat(GUID_WICPixelFormat32bppBGRA, width, height); + } + + img = image(spec); + char* dst = img.data(); + BYTE* src = pixels.data(); + for (int y = 0; y < spec.height; ++y) { + char* dst_x = dst; + for (int x = 0; x < spec.width; ++x, dst_x+=spec.bits_per_pixel/8, ++src) { + *((uint32_t*)dst_x) = (*src < numcolors ? colors[*src] : 0); + } + dst += spec.bytes_per_row; + } + } + else { + img = image(spec); + hr = frame->CopyPixels(nullptr, // Entire bitmap + spec.bytes_per_row, + spec.bytes_per_row * spec.height, + (BYTE*)img.data()); + if (FAILED(hr)) + return false; + } + + + std::swap(*output_image, img); + } + + return true; +} + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from PNG format + +bool read_png(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec) { + return decode(CLSID_WICPngDecoder2, CLSID_WICPngDecoder1, + buf, len, output_image, output_spec); +} + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from JPEG format + +bool read_jpg(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec) +{ + return decode(CLSID_WICJpegDecoder, GUID_NULL, + buf, len, output_image, output_spec); +} + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from GIF format + +bool read_gif(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec) +{ + return decode(CLSID_WICGifDecoder, GUID_NULL, + buf, len, output_image, output_spec); +} + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from BMP format + +bool read_bmp(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec) +{ + return decode(CLSID_WICBmpDecoder, GUID_NULL, + buf, len, output_image, output_spec); +} + +} // namespace win +} // namespace clip diff --git a/clip-lib/clip_win_wic.h b/clip-lib/clip_win_wic.h new file mode 100644 index 0000000..dfef88c --- /dev/null +++ b/clip-lib/clip_win_wic.h @@ -0,0 +1,76 @@ +// Clip Library +// Copyright (c) 2020-2024 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_WIN_WIC_H_INCLUDED +#define CLIP_WIN_WIC_H_INCLUDED +#pragma once + +#if !CLIP_ENABLE_IMAGE + #error This file can be include only when CLIP_ENABLE_IMAGE is defined +#endif + +#include + +#include + +namespace clip { + +class image; +struct image_spec; + +namespace win { + +typedef bool (*ReadWicImageFormatFunc)(const uint8_t*, + const UINT, + clip::image*, + clip::image_spec*); + +ReadWicImageFormatFunc wic_image_format_available(UINT* output_cbformat); + +////////////////////////////////////////////////////////////////////// +// Encode the image as PNG format + +bool write_png_on_stream(const image& image, IStream* stream); + +HGLOBAL write_png(const image& image); + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from PNG format + +bool read_png(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec); + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from JPEG format + +bool read_jpg(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec); + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from GIF format + +bool read_gif(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec); + +////////////////////////////////////////////////////////////////////// +// Decode the clipboard data from BMP format + +bool read_bmp(const uint8_t* buf, + const UINT len, + image* output_image, + image_spec* output_spec); + + +} // namespace win +} // namespace clip + +#endif // CLIP_WIN_WIC_H_INCLUDED diff --git a/clip-lib/clip_x11.cpp b/clip-lib/clip_x11.cpp new file mode 100644 index 0000000..b12d09f --- /dev/null +++ b/clip-lib/clip_x11.cpp @@ -0,0 +1,1123 @@ +// Clip Library +// Copyright (c) 2018-2022 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" +#include "clip_lock_impl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if CLIP_ENABLE_IMAGE && HAVE_PNG_H + #include "clip_x11_png.h" +#endif + +#define CLIP_SUPPORT_SAVE_TARGETS 1 + +namespace clip { + +namespace { + +enum CommonAtom { + ATOM, + INCR, + TARGETS, + CLIPBOARD, +#ifdef HAVE_PNG_H + MIME_IMAGE_PNG, +#endif +#ifdef CLIP_SUPPORT_SAVE_TARGETS + ATOM_PAIR, + SAVE_TARGETS, + MULTIPLE, + CLIPBOARD_MANAGER, +#endif +}; + +const char* kCommonAtomNames[] = { + "ATOM", + "INCR", + "TARGETS", + "CLIPBOARD", +#ifdef HAVE_PNG_H + "image/png", +#endif +#ifdef CLIP_SUPPORT_SAVE_TARGETS + "ATOM_PAIR", + "SAVE_TARGETS", + "MULTIPLE", + "CLIPBOARD_MANAGER", +#endif +}; + +const int kBaseForCustomFormats = 100; + +class Manager { +public: + typedef std::shared_ptr> buffer_ptr; + typedef std::vector atoms; + typedef std::function notify_callback; + + Manager() + : m_lock(m_mutex, std::defer_lock) + , m_connection(xcb_connect(nullptr, nullptr)) + , m_window(0) + , m_incr_process(false) { + if (!m_connection) + return; + + const xcb_setup_t* setup = xcb_get_setup(m_connection); + if (!setup) + return; + + xcb_screen_t* screen = xcb_setup_roots_iterator(setup).data; + if (!screen) + return; + + uint32_t event_mask = + // Just in case that some program reports SelectionNotify events + // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. + XCB_EVENT_MASK_PROPERTY_CHANGE | + // To receive DestroyNotify event and stop the message loop. + XCB_EVENT_MASK_STRUCTURE_NOTIFY; + + m_window = xcb_generate_id(m_connection); + xcb_create_window(m_connection, 0, + m_window, + screen->root, + 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, + XCB_CW_EVENT_MASK, + &event_mask); + + m_thread = std::thread( + [this]{ + process_x11_events(); + }); + } + + ~Manager() { +#ifdef CLIP_SUPPORT_SAVE_TARGETS + if (!m_data.empty() && + m_window && + m_window == get_x11_selection_owner()) { + // If the CLIPBOARD_MANAGER atom is not 0, we assume that there + // is a clipboard manager available were we can leave our data. + xcb_atom_t x11_clipboard_manager = get_atom(CLIPBOARD_MANAGER); + if (x11_clipboard_manager) { + // We have to lock the m_lock mutex that will be used to wait + // the m_cv condition in get_data_from_selection_owner(). + if (try_lock()) { + // Start the SAVE_TARGETS mechanism so the X11 + // CLIPBOARD_MANAGER will save our clipboard data + // from now on. + get_data_from_selection_owner( + { get_atom(SAVE_TARGETS) }, + []() -> bool { return true; }, + x11_clipboard_manager); + } + } + } +#endif + + if (m_window) { + xcb_destroy_window(m_connection, m_window); + xcb_flush(m_connection); + } + + if (m_thread.joinable()) + m_thread.join(); + + if (m_connection) + xcb_disconnect(m_connection); + } + + bool try_lock() { + bool res = m_lock.try_lock(); + if (!res) { + // TODO make this configurable (the same for Windows retries) + for (int i=0; i<5 && !res; ++i) { + res = m_lock.try_lock(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + } + return res; + } + + void unlock() { + m_lock.unlock(); + } + + // Clear our data + void clear_data() { + m_data.clear(); +#if CLIP_ENABLE_IMAGE + m_image.reset(); +#endif + } + + void clear() { + clear_data(); + + // As we want to clear the clipboard content, we set us as the new + // clipboard owner (with an empty clipboard). If this fails, we'll + // try to send a XCB_SELECTION_CLEAR request to the real owner + // (but that can fail anyway because it's a request that the owner + // could ignore). + if (set_x11_selection_owner()) + return; + + // Clear the clipboard data from the selection owner + const xcb_window_t owner = get_x11_selection_owner(); + if (m_window != owner) { + xcb_selection_clear_event_t event; + event.response_type = XCB_SELECTION_CLEAR; + event.pad0 = 0; + event.sequence = 0; + event.time = XCB_CURRENT_TIME; + event.owner = owner; + event.selection = get_atom(CLIPBOARD); + + xcb_send_event(m_connection, false, + owner, + XCB_EVENT_MASK_NO_EVENT, + (const char*)&event); + + xcb_flush(m_connection); + } + } + + bool is_convertible(format f) const { + const atoms atoms = get_format_atoms(f); + const xcb_window_t owner = get_x11_selection_owner(); + + // If we are the owner, we just can check the m_data map + if (owner == m_window) { + for (xcb_atom_t atom : atoms) { + auto it = m_data.find(atom); + if (it != m_data.end()) + return true; + } + } + // Ask to the selection owner the available formats/atoms/targets. + else if (owner) { + return + get_data_from_selection_owner( + { get_atom(TARGETS) }, + [this, &atoms]() -> bool { + assert(m_reply_data); + if (!m_reply_data) + return false; + + const xcb_atom_t* sel_atoms = (const xcb_atom_t*)&(*m_reply_data)[0]; + int sel_natoms = m_reply_data->size() / sizeof(xcb_atom_t); + auto atoms_begin = atoms.begin(); + auto atoms_end = atoms.end(); + for (int i=0; i>(len); + std::copy(buf, + buf+len, + shared_data_buf->begin()); + for (xcb_atom_t atom : atoms) + m_data[atom] = shared_data_buf; + + return true; + } + + bool get_data(format f, char* buf, size_t len) const { + const atoms atoms = get_format_atoms(f); + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + for (xcb_atom_t atom : atoms) { + auto it = m_data.find(atom); + if (it != m_data.end()) { + size_t n = std::min(len, it->second->size()); + std::copy(it->second->begin(), + it->second->begin()+n, + buf); + + if (f == text_format()) { + // Add an extra null char + if (n < len) + buf[n] = 0; + } + + return true; + } + } + } + else if (owner) { + if (get_data_from_selection_owner( + atoms, + [this, buf, len, f]() -> bool { + size_t n = std::min(len, m_reply_data->size()); + std::copy(m_reply_data->begin(), + m_reply_data->begin()+n, + buf); + + if (f == text_format()) { + if (n < len) + buf[n] = 0; // Include a null character + } + + return true; + })) { + return true; + } + } + return false; + } + + size_t get_data_length(format f) const { + size_t len = 0; + const atoms atoms = get_format_atoms(f); + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + for (xcb_atom_t atom : atoms) { + auto it = m_data.find(atom); + if (it != m_data.end()) { + len = it->second->size(); + break; + } + } + } + else if (owner) { + if (!get_data_from_selection_owner( + atoms, + [this, &len]() -> bool { + len = m_reply_data->size(); + return true; + })) { + // Error getting data length + return 0; + } + } + if (f == text_format() && len > 0) { + ++len; // Add an extra byte for the null char + } + return len; + } + +#if CLIP_ENABLE_IMAGE + + bool set_image(const image& image) { + if (!set_x11_selection_owner()) + return false; + + m_image = image; + +#ifdef HAVE_PNG_H + // Put a nullptr in the m_data for image/png format and then we'll + // encode the png data when the image is requested in this format. + m_data[get_atom(MIME_IMAGE_PNG)] = buffer_ptr(); +#endif + + return true; + } + + bool get_image(image& output_img) const { + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + if (m_image.is_valid()) { + output_img = m_image; + return true; + } + } +#ifdef HAVE_PNG_H + else if (owner && + get_data_from_selection_owner( + { get_atom(MIME_IMAGE_PNG) }, + [this, &output_img]() -> bool { + return x11::read_png(&(*m_reply_data)[0], + m_reply_data->size(), + &output_img, nullptr); + })) { + return true; + } +#endif + return false; + } + + bool get_image_spec(image_spec& spec) const { + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + if (m_image.is_valid()) { + spec = m_image.spec(); + return true; + } + } +#ifdef HAVE_PNG_H + else if (owner && + get_data_from_selection_owner( + { get_atom(MIME_IMAGE_PNG) }, + [this, &spec]() -> bool { + return x11::read_png(&(*m_reply_data)[0], + m_reply_data->size(), + nullptr, &spec); + })) { + return true; + } +#endif + return false; + } + +#endif // CLIP_ENABLE_IMAGE + + format register_format(const std::string& name) { + xcb_atom_t atom = get_atom(name.c_str()); + m_custom_formats.push_back(atom); + return (format)(m_custom_formats.size()-1) + kBaseForCustomFormats; + } + +private: + + void process_x11_events() { + bool stop = false; + xcb_generic_event_t* event; + while (!stop && (event = xcb_wait_for_event(m_connection))) { + int type = (event->response_type & ~0x80); + + switch (type) { + + case XCB_DESTROY_NOTIFY: + // To stop the message loop we can just destroy the window + stop = true; + break; + + // Someone else has new content in the clipboard, so is + // notifying us that we should delete our data now. + case XCB_SELECTION_CLEAR: + handle_selection_clear_event( + (xcb_selection_clear_event_t*)event); + break; + + // Someone is requesting the clipboard content from us. + case XCB_SELECTION_REQUEST: + handle_selection_request_event( + (xcb_selection_request_event_t*)event); + break; + + // We've requested the clipboard content and this is the + // answer. + case XCB_SELECTION_NOTIFY: + handle_selection_notify_event( + (xcb_selection_notify_event_t*)event); + break; + + case XCB_PROPERTY_NOTIFY: + handle_property_notify_event( + (xcb_property_notify_event_t*)event); + break; + + } + + free(event); + } + } + + void handle_selection_clear_event(xcb_selection_clear_event_t* event) { + if (event->selection == get_atom(CLIPBOARD)) { + std::lock_guard lock(m_mutex); + clear_data(); // Clear our clipboard data + } + } + + void handle_selection_request_event(xcb_selection_request_event_t* event) { + std::lock_guard lock(m_mutex); + + if (event->target == get_atom(TARGETS)) { + atoms targets; + targets.push_back(get_atom(TARGETS)); +#ifdef CLIP_SUPPORT_SAVE_TARGETS + targets.push_back(get_atom(SAVE_TARGETS)); + targets.push_back(get_atom(MULTIPLE)); +#endif + for (const auto& it : m_data) + targets.push_back(it.first); + + // Set the "property" of "requestor" with the clipboard + // formats ("targets", atoms) that we provide. + xcb_change_property( + m_connection, + XCB_PROP_MODE_REPLACE, + event->requestor, + event->property, + get_atom(ATOM), + 8*sizeof(xcb_atom_t), + targets.size(), + &targets[0]); + } +#ifdef CLIP_SUPPORT_SAVE_TARGETS + else if (event->target == get_atom(SAVE_TARGETS)) { + // Do nothing + } + else if (event->target == get_atom(MULTIPLE)) { + xcb_get_property_reply_t* reply = + get_and_delete_property(event->requestor, + event->property, + get_atom(ATOM_PAIR), + false); + if (reply) { + for (xcb_atom_t + *ptr=(xcb_atom_t*)xcb_get_property_value(reply), + *end=ptr + (xcb_get_property_value_length(reply)/sizeof(xcb_atom_t)); + ptrrequestor, + property, + target)) { + xcb_change_property( + m_connection, + XCB_PROP_MODE_REPLACE, + event->requestor, + event->property, + XCB_ATOM_NONE, 0, 0, nullptr); + } + } + + free(reply); + } + } +#endif // CLIP_SUPPORT_SAVE_TARGETS + else { + if (!set_requestor_property_with_clipboard_content( + event->requestor, + event->property, + event->target)) { + // If the requested "target" type is not present in our + // clipboard, we continue normally sending a SelectionNotify + // to the "requestor" anyway because some text editors + // (e.g. Emacs) request the TIMESTAMP target (without asking + // if it's present in TARGETS) after asking for UTF8_STRING. + // + // Sending the SelectionNotify will wake up the "requestor" + // that is asking for the clipboard content. In this way we + // avoid a "Timed out waiting for reply from selection owner" + // error in Emacs (and probably other text editors). + } + } + + // Notify the "requestor" that we've already updated the property. + xcb_selection_notify_event_t notify; + notify.response_type = XCB_SELECTION_NOTIFY; + notify.pad0 = 0; + notify.sequence = 0; + notify.time = event->time; + notify.requestor = event->requestor; + notify.selection = event->selection; + notify.target = event->target; + notify.property = event->property; + + xcb_send_event(m_connection, false, + event->requestor, + XCB_EVENT_MASK_NO_EVENT, // SelectionNotify events go without mask + (const char*)¬ify); + + xcb_flush(m_connection); + } + + bool set_requestor_property_with_clipboard_content(const xcb_atom_t requestor, + const xcb_atom_t property, + const xcb_atom_t target) { + auto it = m_data.find(target); + if (it == m_data.end()) { + // Nothing to do (unsupported target) + return false; + } + + // This can be null of the data was set from an image but we + // didn't encode the image yet (e.g. to image/png format). + if (!it->second) { + encode_data_on_demand(*it); + + // Return nothing, the given "target" cannot be constructed + // (maybe by some encoding error). + if (!it->second) + return false; + } + + // Set the "property" of "requestor" with the + // clipboard content in the requested format ("target"). + xcb_change_property( + m_connection, + XCB_PROP_MODE_REPLACE, + requestor, + property, + target, + 8, + it->second->size(), + &(*it->second)[0]); + return true; + } + + void handle_selection_notify_event(xcb_selection_notify_event_t* event) { + assert(event->requestor == m_window); + + if (event->target == get_atom(TARGETS)) + m_target_atom = get_atom(ATOM); + else + m_target_atom = event->target; + + xcb_get_property_reply_t* reply = + get_and_delete_property(event->requestor, + event->property, + m_target_atom); + if (reply) { + // In this case, We're going to receive the clipboard content in + // chunks of data with several PropertyNotify events. + if (reply->type == get_atom(INCR)) { + free(reply); + + reply = get_and_delete_property(event->requestor, + event->property, + get_atom(INCR)); + if (reply) { + if (xcb_get_property_value_length(reply) == 4) { + uint32_t n = *(uint32_t*)xcb_get_property_value(reply); + m_reply_data = std::make_shared>(n); + m_reply_offset = 0; + m_incr_process = true; + m_incr_received = true; + } + free(reply); + } + } + else { + // Simple case, the whole clipboard content in just one reply + // (without the INCR method). + m_reply_data.reset(); + m_reply_offset = 0; + copy_reply_data(reply); + + call_callback(reply); + + free(reply); + } + } + } + + void handle_property_notify_event(xcb_property_notify_event_t* event) { + if (m_incr_process && + event->state == XCB_PROPERTY_NEW_VALUE && + event->atom == get_atom(CLIPBOARD)) { + xcb_get_property_reply_t* reply = + get_and_delete_property(event->window, + event->atom, + m_target_atom); + if (reply) { + m_incr_received = true; + + // When the length is 0 it means that the content was + // completely sent by the selection owner. + if (xcb_get_property_value_length(reply) > 0) { + copy_reply_data(reply); + } + else { + // Now that m_reply_data has the complete clipboard content, + // we can call the m_callback. + call_callback(reply); + m_incr_process = false; + } + free(reply); + } + } + } + + xcb_get_property_reply_t* get_and_delete_property(xcb_window_t window, + xcb_atom_t property, + xcb_atom_t atom, + bool delete_prop = true) { + xcb_get_property_cookie_t cookie = + xcb_get_property(m_connection, + delete_prop, + window, + property, + atom, + 0, 0x1fffffff); // 0x1fffffff = INT32_MAX / 4 + + xcb_generic_error_t* err = nullptr; + xcb_get_property_reply_t* reply = + xcb_get_property_reply(m_connection, cookie, &err); + if (err) { + // TODO report error + free(err); + } + return reply; + } + + // Concatenates the new data received in "reply" into "m_reply_data" + // buffer. + void copy_reply_data(xcb_get_property_reply_t* reply) { + const uint8_t* src = (const uint8_t*)xcb_get_property_value(reply); + // n = length of "src" in bytes + size_t n = xcb_get_property_value_length(reply); + + size_t req = m_reply_offset+n; + if (!m_reply_data) { + m_reply_data = std::make_shared>(req); + } + // The "m_reply_data" size can be smaller because the size + // specified in INCR property is just a lower bound. + else if (req > m_reply_data->size()) { + m_reply_data->resize(req); + } + + std::copy(src, src+n, m_reply_data->begin()+m_reply_offset); + m_reply_offset += n; + } + + // Calls the current m_callback() to handle the clipboard content + // received from the owner. + void call_callback(xcb_get_property_reply_t* reply) { + m_callback_result = false; + if (m_callback) + m_callback_result = m_callback(); + + m_cv.notify_one(); + + m_reply_data.reset(); + } + + bool get_data_from_selection_owner(const atoms& atoms, + const notify_callback&& callback, + xcb_atom_t selection = 0) const { + if (!selection) + selection = get_atom(CLIPBOARD); + + // Put the callback on "m_callback" so we can call it on + // SelectionNotify event. + m_callback = std::move(callback); + + // Clear data if we are not the selection owner. + if (m_window != get_x11_selection_owner()) + m_data.clear(); + + // Ask to the selection owner for its content on each known + // text format/atom. + for (xcb_atom_t atom : atoms) { + xcb_convert_selection(m_connection, + m_window, // Send us the result + selection, // Clipboard selection + atom, // The clipboard format that we're requesting + get_atom(CLIPBOARD), // Leave result in this window's property + XCB_CURRENT_TIME); + + xcb_flush(m_connection); + + // We use the "m_incr_received" to wait several timeouts in case + // that we've received the INCR SelectionNotify or + // PropertyNotify events. + do { + m_incr_received = false; + + // Wait a response for 100 milliseconds + std::cv_status status = + m_cv.wait_for(m_lock, + std::chrono::milliseconds(get_x11_wait_timeout())); + if (status == std::cv_status::no_timeout) { + // If the condition variable was notified, it means that the + // callback was called correctly. + return m_callback_result; + } + } while (m_incr_received); + } + + // Reset callback + m_callback = notify_callback(); + return false; + } + + atoms get_atoms(const char** names, + const int n) const { + atoms result(n, 0); + std::vector cookies(n); + + for (int i=0; isecond; + else + cookies[i] = xcb_intern_atom( + m_connection, 0, + std::strlen(names[i]), names[i]); + } + + for (int i=0; iatom; + free(reply); + } + } + } + + return result; + } + + xcb_atom_t get_atom(const char* name) const { + auto it = m_atoms.find(name); + if (it != m_atoms.end()) + return it->second; + + xcb_atom_t result = 0; + xcb_intern_atom_cookie_t cookie = + xcb_intern_atom(m_connection, 0, + std::strlen(name), name); + + xcb_intern_atom_reply_t* reply = + xcb_intern_atom_reply(m_connection, + cookie, + nullptr); + if (reply) { + result = m_atoms[name] = reply->atom; + free(reply); + } + return result; + } + + xcb_atom_t get_atom(CommonAtom i) const { + if (m_common_atoms.empty()) { + m_common_atoms = + get_atoms(kCommonAtomNames, + sizeof(kCommonAtomNames) / sizeof(kCommonAtomNames[0])); + } + return m_common_atoms[i]; + } + + const atoms& get_text_format_atoms() const { + if (m_text_atoms.empty()) { + const char* names[] = { + // Prefer utf-8 formats first + "UTF8_STRING", + "text/plain;charset=utf-8", + "text/plain;charset=UTF-8", + "GTK_TEXT_BUFFER_CONTENTS", // Required for gedit (and maybe gtk+ apps) + // ANSI C strings? + "STRING", + "TEXT", + "text/plain", + }; + m_text_atoms = get_atoms(names, sizeof(names) / sizeof(names[0])); + } + return m_text_atoms; + } + +#if CLIP_ENABLE_IMAGE + + const atoms& get_image_format_atoms() const { + if (m_image_atoms.empty()) { +#ifdef HAVE_PNG_H + m_image_atoms.push_back(get_atom(MIME_IMAGE_PNG)); +#endif + } + return m_image_atoms; + } + +#endif // CLIP_ENABLE_IMAGE + + atoms get_format_atoms(const format f) const { + atoms atoms; + if (f == text_format()) { + atoms = get_text_format_atoms(); + } +#if CLIP_ENABLE_IMAGE + else if (f == image_format()) { + atoms = get_image_format_atoms(); + } +#endif // CLIP_ENABLE_IMAGE + else { + xcb_atom_t atom = get_format_atom(f); + if (atom) + atoms.push_back(atom); + } + return atoms; + } + +#if !defined(NDEBUG) + // This can be used to print debugging messages. + std::string get_atom_name(xcb_atom_t atom) const { + std::string result; + xcb_get_atom_name_cookie_t cookie = + xcb_get_atom_name(m_connection, atom); + xcb_generic_error_t* err = nullptr; + xcb_get_atom_name_reply_t* reply = + xcb_get_atom_name_reply(m_connection, cookie, &err); + if (err) { + free(err); + } + if (reply) { + int len = xcb_get_atom_name_name_length(reply); + if (len > 0) { + result.resize(len); + char* name = xcb_get_atom_name_name(reply); + if (name) + std::copy(name, name+len, result.begin()); + } + free(reply); + } + return result; + } +#endif + + bool set_x11_selection_owner() const { + xcb_void_cookie_t cookie = + xcb_set_selection_owner_checked(m_connection, + m_window, + get_atom(CLIPBOARD), + XCB_CURRENT_TIME); + xcb_generic_error_t* err = + xcb_request_check(m_connection, + cookie); + if (err) { + free(err); + return false; + } + return true; + } + + xcb_window_t get_x11_selection_owner() const { + xcb_window_t result = 0; + xcb_get_selection_owner_cookie_t cookie = + xcb_get_selection_owner(m_connection, + get_atom(CLIPBOARD)); + + xcb_get_selection_owner_reply_t* reply = + xcb_get_selection_owner_reply(m_connection, cookie, nullptr); + if (reply) { + result = reply->owner; + free(reply); + } + return result; + } + + xcb_atom_t get_format_atom(const format f) const { + int i = f - kBaseForCustomFormats; + if (i >= 0 && i < int(m_custom_formats.size())) + return m_custom_formats[i]; + else + return 0; + } + + void encode_data_on_demand(std::pair& e) { +#if defined(CLIP_ENABLE_IMAGE) && defined(HAVE_PNG_H) + if (e.first == get_atom(MIME_IMAGE_PNG)) { + assert(m_image.is_valid()); + if (!m_image.is_valid()) + return; + + std::vector output; + if (x11::write_png(m_image, output)) { + e.second = + std::make_shared>( + std::move(output)); + } + // else { TODO report png conversion errors } + } +#endif // defined(CLIP_ENABLE_IMAGE) && defined(HAVE_PNG_H) + } + + // Access to the whole Manager + std::mutex m_mutex; + + // Lock used in the main thread using the Manager (i.e. by lock::impl) + mutable std::unique_lock m_lock; + + // Connection to X11 server + xcb_connection_t* m_connection; + + // Temporal background window used to own the clipboard and process + // all events related about the clipboard in a background thread + xcb_window_t m_window; + + // Used to wait/notify the arrival of the SelectionNotify event when + // we requested the clipboard content from other selection owner. + mutable std::condition_variable m_cv; + + // Thread used to run a background message loop to wait X11 events + // about clipboard. The X11 selection owner will be a hidden window + // created by us just for the clipboard purpose/communication. + std::thread m_thread; + + // Internal callback used when a SelectionNotify is received (or the + // whole data content is received by the INCR method). So this + // callback can use the notification by different purposes (e.g. get + // the data length only, or get/process the data content, etc.). + mutable notify_callback m_callback; + + // Result returned by the m_callback. Used as return value in the + // get_data_from_selection_owner() function. For example, if the + // callback must read a "image/png" file from the clipboard data and + // fails, the callback can return false and finally the get_image() + // will return false (i.e. there is data, but it's not a valid image + // format). + std::atomic m_callback_result; + + // Cache of known atoms + mutable std::map m_atoms; + + // Cache of common used atoms by us + mutable atoms m_common_atoms; + + // Cache of atoms related to text or image content + mutable atoms m_text_atoms; +#if CLIP_ENABLE_IMAGE + mutable atoms m_image_atoms; +#endif + + // Actual clipboard data generated by us (when we "copy" content in + // the clipboard, it means that we own the X11 "CLIPBOARD" + // selection, and in case of SelectionRequest events, we've to + // return the data stored in this "m_data" field) + mutable std::map m_data; + + // Copied image in the clipboard. As we have to transfer the image + // in some specific format (e.g. image/png) we want to keep a copy + // of the image and make the conversion when the clipboard data is + // requested by other process. +#if CLIP_ENABLE_IMAGE + mutable image m_image; +#endif + + // True if we have received an INCR notification so we're going to + // process several PropertyNotify to concatenate all data chunks. + bool m_incr_process; + + // Variable used to wait more time if we've received an INCR + // notification, which means that we're going to receive large + // amounts of data from the selection owner. + mutable bool m_incr_received; + + // Target/selection format used in the SelectionNotify. Used in the + // INCR method to get data from the same property in the same format + // (target) on each PropertyNotify. + xcb_atom_t m_target_atom; + + // Each time we receive data from the selection owner, we put that + // data in this buffer. If we get the data with the INCR method, + // we'll concatenate chunks of data in this buffer to complete the + // whole clipboard content. + buffer_ptr m_reply_data; + + // Used to concatenate chunks of data in "m_reply_data" from several + // PropertyNotify when we are getting the selection owner data with + // the INCR method. + size_t m_reply_offset; + + // List of user-defined formats/atoms. + std::vector m_custom_formats; +}; + +Manager* manager = nullptr; + +void delete_manager_atexit() { + if (manager) { + delete manager; + manager = nullptr; + } +} + +Manager* get_manager() { + if (!manager) { + manager = new Manager; + std::atexit(delete_manager_atexit); + } + return manager; +} + +} // anonymous namespace + +lock::impl::impl(void*) : m_locked(false) { + m_locked = get_manager()->try_lock(); +} + +lock::impl::~impl() { + if (m_locked) + manager->unlock(); +} + +bool lock::impl::clear() { + manager->clear(); + return true; +} + +bool lock::impl::is_convertible(format f) const { + return manager->is_convertible(f); +} + +bool lock::impl::set_data(format f, const char* buf, size_t len) { + return manager->set_data(f, buf, len); +} + +bool lock::impl::get_data(format f, char* buf, size_t len) const { + return manager->get_data(f, buf, len); +} + +size_t lock::impl::get_data_length(format f) const { + return manager->get_data_length(f); +} + +#if CLIP_ENABLE_IMAGE + +bool lock::impl::set_image(const image& image) { + return manager->set_image(image); +} + +bool lock::impl::get_image(image& output_img) const { + return manager->get_image(output_img); +} + +bool lock::impl::get_image_spec(image_spec& spec) const { + return manager->get_image_spec(spec); +} + +#endif // CLIP_ENABLE_IMAGE + +format register_format(const std::string& name) { + return get_manager()->register_format(name); +} + +} // namespace clip diff --git a/clip-lib/clip_x11_png.h b/clip-lib/clip_x11_png.h new file mode 100644 index 0000000..cf3b514 --- /dev/null +++ b/clip-lib/clip_x11_png.h @@ -0,0 +1,230 @@ +// Clip Library +// Copyright (c) 2018-2021 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" + +#include +#include + +#include "png.h" + +namespace clip { +namespace x11 { + +////////////////////////////////////////////////////////////////////// +// Functions to convert clip::image into png data to store it in the +// clipboard. + +void write_data_fn(png_structp png, png_bytep buf, png_size_t len) { + std::vector& output = *(std::vector*)png_get_io_ptr(png); + const size_t i = output.size(); + output.resize(i+len); + std::copy(buf, buf+len, output.begin()+i); +} + +bool write_png(const image& image, + std::vector& output) { + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, + nullptr, nullptr, nullptr); + if (!png) + return false; + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_write_struct(&png, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png))) { + png_destroy_write_struct(&png, &info); + return false; + } + + png_set_write_fn(png, + (png_voidp)&output, + write_data_fn, + nullptr); // No need for a flush function + + const image_spec& spec = image.spec(); + int color_type = (spec.alpha_mask ? + PNG_COLOR_TYPE_RGB_ALPHA: + PNG_COLOR_TYPE_RGB); + + png_set_IHDR(png, info, + spec.width, spec.height, 8, color_type, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_write_info(png, info); + png_set_packing(png); + + png_bytep row = + (png_bytep)png_malloc(png, png_get_rowbytes(png, info)); + + for (png_uint_32 y=0; y> spec.red_shift; + *(dst++) = (c & spec.green_mask) >> spec.green_shift; + *(dst++) = (c & spec.blue_mask ) >> spec.blue_shift; + if (color_type == PNG_COLOR_TYPE_RGB_ALPHA) + *(dst++) = (c & spec.alpha_mask) >> spec.alpha_shift; + } + + png_write_rows(png, &row, 1); + } + + png_free(png, row); + png_write_end(png, info); + png_destroy_write_struct(&png, &info); + return true; +} + +////////////////////////////////////////////////////////////////////// +// Functions to convert png data stored in the clipboard to a +// clip::image. + +struct read_png_io { + const uint8_t* buf; + size_t len; + size_t pos; +}; + +void read_data_fn(png_structp png, png_bytep buf, png_size_t len) { + read_png_io& io = *(read_png_io*)png_get_io_ptr(png); + if (io.pos < io.len) { + size_t n = std::min(len, io.len-io.pos); + if (n > 0) { + std::copy(io.buf+io.pos, + io.buf+io.pos+n, + buf); + io.pos += n; + } + } +} + +bool read_png(const uint8_t* buf, + const size_t len, + image* output_image, + image_spec* output_spec) { + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, + nullptr, nullptr, nullptr); + if (!png) + return false; + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_read_struct(&png, nullptr, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png))) { + png_destroy_read_struct(&png, &info, nullptr); + return false; + } + + read_png_io io = { buf, len, 0 }; + png_set_read_fn(png, (png_voidp)&io, read_data_fn); + + png_read_info(png, info); + + png_uint_32 width, height; + int bit_depth, color_type, interlace_type; + png_get_IHDR(png, info, &width, &height, + &bit_depth, &color_type, + &interlace_type, + nullptr, nullptr); + + image_spec spec; + spec.width = width; + spec.height = height; + spec.bits_per_pixel = 32; + + // Don't use png_get_rowbytes(png, info) here because this is the + // bytes_per_row of the output clip::image (the png file could + // contain 24bpp but we want to return a 32bpp anyway with alpha=255 + // in that case). + spec.bytes_per_row = 4*width; + + spec.red_mask = 0x000000ff; + spec.green_mask = 0x0000ff00; + spec.blue_mask = 0x00ff0000; + spec.red_shift = 0; + spec.green_shift = 8; + spec.blue_shift = 16; + + if ((color_type & PNG_COLOR_MASK_ALPHA) == PNG_COLOR_MASK_ALPHA) { + spec.alpha_mask = 0xff000000; + spec.alpha_shift = 24; + } + else { + spec.alpha_mask = 0; + spec.alpha_shift = 0; + } + + if (output_spec) + *output_spec = spec; + + if (output_image && + width > 0 && + height > 0) { + image img(spec); + + // We want RGB 24-bit or RGBA 32-bit as a result + png_set_strip_16(png); // Down to 8-bit (TODO we might support 16-bit values) + png_set_packing(png); // Use one byte if color depth < 8-bit + png_set_expand_gray_1_2_4_to_8(png); + png_set_palette_to_rgb(png); + png_set_gray_to_rgb(png); + png_set_tRNS_to_alpha(png); + + int number_passes = png_set_interlace_handling(png); + png_read_update_info(png, info); + + const int src_bytes_per_row = png_get_rowbytes(png, info); + png_bytepp rows = (png_bytepp)png_malloc(png, sizeof(png_bytep)*height); + png_uint_32 y; + for (y=0; y 0) + n += 4 - (n % 4); + else + ++n; + } + + return n; +} + +image::image() + : m_own_data(false), + m_data(nullptr) +{ +} + +image::image(const image_spec& spec) + : m_own_data(true), + m_data(new char[spec.required_data_size()]), + m_spec(spec) { +} + +image::image(const void* data, const image_spec& spec) + : m_own_data(false), + m_data((char*)data), + m_spec(spec) { +} + +image::image(const image& image) + : m_own_data(false), + m_data(nullptr), + m_spec(image.m_spec) { + copy_image(image); +} + +image::image(image&& image) + : m_own_data(false), + m_data(nullptr) { + move_image(std::move(image)); +} + +image::~image() { + reset(); +} + +image& image::operator=(const image& image) { + copy_image(image); + return *this; +} + +image& image::operator=(image&& image) { + move_image(std::move(image)); + return *this; +} + +void image::reset() { + if (m_own_data) { + delete[] m_data; + m_own_data = false; + m_data = nullptr; + } +} + +void image::copy_image(const image& image) { + reset(); + + m_spec = image.spec(); + std::size_t n = m_spec.required_data_size(); + + m_own_data = true; + m_data = new char[n]; + std::copy(image.data(), + image.data()+n, + m_data); +} + +void image::move_image(image&& image) { + std::swap(m_own_data, image.m_own_data); + std::swap(m_data, image.m_data); + std::swap(m_spec, image.m_spec); +} + +} // namespace clip diff --git a/owo.cpp b/owo.cpp index e70e0b0..e1a8bce 100644 --- a/owo.cpp +++ b/owo.cpp @@ -2,7 +2,7 @@ #include #include #include -#include "clip/clip.h" +#include "clip-lib/clip.h" using namespace std; string replacements[6][2] = {