I'm trying to grab a screenshot of the portion of a window using xcb_shm_get_image_unchecked
. I've created the shared memory using the following code:
#include <cstdlib>
#include <memory>
#include <sys/shm.h>
#include <xcb/shm.h>
#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#include <xcb/xcb_pixel.h>
#include <xcb/xproto.h>
#include <spdlog/spdlog.h>
const auto IMAGE_WIDTH = 640;
const auto IMAGE_HEIGHT = 480;
const auto SHM_SIZE = 4 * 1024 * 1024;
auto main() -> int
{
std::unique_ptr<xcb_connection_t, decltype(&xcb_disconnect)> c(
xcb_connect(nullptr, nullptr), &xcb_disconnect);
if (!c) {
spdlog::error("failed to connect to X server");
return EXIT_FAILURE;
}
auto roots_iter = xcb_setup_roots_iterator(xcb_get_setup(c.get()));
if (roots_iter.rem == 0) {
spdlog::error("no screen found");
return EXIT_FAILURE;
}
auto root = roots_iter.data->root;
xcb_shm_seg_t shmseg = xcb_generate_id(c.get());
// xcb_shm_get_im
auto shm_reply = xcb_shm_create_segment_reply(
c.get(), xcb_shm_create_segment(c.get(), shmseg, SHM_SIZE, 0), nullptr);
if (!shm_reply) {
spdlog::error("failed to create shared memory segment");
return EXIT_FAILURE;
}
auto fds = xcb_shm_create_segment_reply_fds(c.get(), shm_reply);
spdlog::info("found {} fds", shm_reply->nfd);
for (int i = 0; i < shm_reply->nfd; i++) {
auto err = xcb_request_check(
c.get(), xcb_shm_attach_fd(c.get(), shmseg, fds[i], true));
if (err) {
spdlog::error("failed to attach fd: {}", fds[i]);
delete err;
}
}
for (int i = 0; i < shm_reply->nfd; i++) {
close(fds[i]);
}
xcb_shm_detach(c.get(), shmseg);
return 0;
}
First question: have I done this right in terms of freeing up resources?
2nd: How do I get access to the shared memory segment?
Finally, how would I grab a 640x480 screenshot of the root window? Am I on the right path with this:
auto image = xcb_shm_get_image_reply(
c.get(),
xcb_shm_get_image_unchecked(c.get(),
root,
0,
0,
IMAGE_WIDTH,
IMAGE_HEIGHT,
XCB_GC_PLANE_MASK,
XCB_IMAGE_FORMAT_Z_PIXMAP,
shmseg,
0),
nullptr);
if (!image) {
spdlog::error("failed to get image");
xcb_shm_detach(c.get(), shmseg);
return EXIT_FAILURE;
}
spdlog::info("image: size {}", image->size);
delete image;
You have to make a call to mmap
to get access to the shared memory. From the docs for xcb_shm_create_segment_unchecked
:
Asks the server to allocate a shared memory segment. The server’s reply will include a file descriptor for the client to pass to mmap().
And how do you get access?
You have to make a call to mmap
including the file descriptor to tell mmap where to find the region of memory which was allocated by X
server.
You can do it using something like this:
auto *shmem = mmap(nullptr, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fds[0], 0);
if (shmem == MAP_FAILED) {
... // cleanup
}
close(fds[0]);
The arguments should be pretty self-explanatory once you read the docs for mmap
. The last 0 just means that we want the pointer to refer to the start of the memory region.
Of course, don't forget to munmap
when you're finished:
munmap(p, SHM_SIZE);
Finally, there is no need for xcb_shm_attach_fd(c.get(), shmseg, fds[i], true));
. This is only needed if you had created the mmap file yourself, and passing it to the X server so that it can have access.
First question: have I done this right in terms of freeing up resources?
Yes you have
how would I grab a 640x480 screenshot of the root window?
The code you have for grabbing the image is almost right. Just change the plane_mask
argument to ~0
. You can see it's similar to how cairo
does it, and also how obs
does it.
To actually convert the bytes to an image, you can choose to convert it to a lowest common denominator image format like PPM
. Here is what that code could look like:
if (auto ppmFile = std::ofstream("/path/to/screenshot.ppm", std::ios::binary); ppmFile) {
ppmFile << "P6\n" << IMAGE_WIDTH << " " << IMAGE_HEIGHT << "\n255\n";
const auto *const imageData = shmem;
for (uint32_t rgb = 0; rgb + 4 <= image->size; rgb += 4) {
auto b = imageData[rgb];
auto g = imageData[rgb + 1];
auto r = imageData[rgb + 2];
if (setup->image_byte_order == XCB_IMAGE_ORDER_MSB_FIRST) {
std::swap(b, r);
}
ppmFile << r << g << b;
}
}
The end result should be an image in ppm
format located at /path/to/screenshot.ppm
. If you don't have a way of viewing it, imagemagick is able to convert ppm to png, so you can view it that way.