// 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