This is an old revision of the document!
By now, we all know that not all files are backed by a storage device. While some reside on your HDD/SSD, Linux mounts virtual filesystems such as procfs or sysfs. Today we are going to look at a special file called /dev/fb0. This file is an abstraction of the frame buffer used by your video hardware to render your screen. In it, each pixel is represented as a RGBA (Red-Green-Blue-Alpha) value. Creating a copy of this file to persistent storage will effectively take a screenshot. Unfortunately, this screenshot will not be in any known format (e.g.: JPEG, PNG, etc.) A bit of work still needs to be done to properly format it such that the appropriate software (e.g.: gwenview) can interpret and display it.
In this first exercise, we are going to write a Python3 script that does just that: take the content of /dev/fb0 and output an image file in a format of your choosing. To that end, we are going to use the PIL module.
First of all, we are going to set up a virtual environment for us to work in. If you need a reminder on what a venv actually is, refer to this lab.
If you are on Ubuntu, you must first install some prerequisites:
$ sudo apt update $ sudo apt install libzlib1g-dev libjpeg-dev
# this will be our workspace directory # NOTE: !$ is substituted with the last argument of your previous command $ mkdir frame_buffer $ cd !$ # create venv and activate it $ python3 -m venv .venv $ source .venv/bin/activate # install pillow (an actively maintained fork of PIL) $ pip3 install pillow
To start things off, we are going to use the following skeleton:
#!.venv/bin/python3 import argparse # argument parsing import struct # data unpacking from PIL import Image # image processing def main(): # parse cli arguments parser = argparse.ArgumentParser() parser.add_argument('FILE', help='output image file') parser.add_argument('--src', help='data source', default='/dev/fb0', metavar='/dev/fb*') parser.add_argument('--width', help='screen width [px]', type=int, default=1920, metavar=' INT') parser.add_argument('--height', help='screen height [px]', type=int, default=1080, metavar='INT') cfg = parser.parse_args() # TODO 1: read contents of cfg.src (the frame buffer) # TODO 2: split data in groups of 4 bytes # create a new PIL Image object img = Image.new('RGB', (cfg.width, cfg.height)) px = img.load() # set each pixel value for i in range(cfg.width): for j in range(cfg.height): # TODO 3: write each pixel value in px[i,j] as a RGB tuple # NOTE : the four bytes in the groups that you split previously # are in fact in BGRA format; we don't need the Alpha # value but the other three bytes must be revered pass # save image do disk # NOTE: format will be determined from the file's extension img.save(cfg.FILE, None) # clean up PIL Image object img.close() if __name__ == '__main__': main()
The script uses the argparse module to interpret some command-line arguments (the --help
flag is implied). If you feel like adding something to these, you're free to do so. It's a good idea to make your script as interactive as possible. The parsed arguments (or their default values) are stored as members of the cfg object. If you're unsure how to access them, just print the whole object once to get a feel for what it contains.
For now, go through each TODO in the skeleton and fill in the blanks. Next are a few hints that might help you. Feel free to ask for help if you get stuck :)
This step is pretty straightforward. Remember that you are working with a binary file!
The result from the previous step will be a byte array. In order to split it into an array of 4-byte RGBA sub-arrays, you can use list comprehension (discussed in this lab. If you have other ideas, you're free to do your own thing.
While img is a PIL abstraction of the image you wish to create, individual pixels will be accessed via px, an instance of the PixelAccess class in PIL. Each pixel is represented as a tuple of three integers (i.e.: not bytes). To unpack each value from the byte-array corresponding to your pixel, you can use the struct module. Search in the documentation for the format character representing an unsigned char
(equivalent to uint8_t
).
Remember that you don't have to get the script right from your first try. If you're not familiar with a module, experiment with it a bit in a python shell. Or better yet, use ipython.
Finally, it's time to test the script. Notice how your script takes the --width
and --height
optional arguments (with values defaulting to 1920 and 1080). If you have another resolution but you're not sure what that is, it's not hard to find out:
$ xrandr --prop | grep primary eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
Note that /dev/fb0 has restricted access permissions for non-owners, so you need to run your script with sudo:
$ ls -l /dev/fb0 crw-rw---- 1 root video 29, 0 Apr 14 17:45 /dev/fb0 # grant execute permissions if you haven't already $ chmod +x fb2img.py $ sudo ./fb2img.py screenshot.png
Now let's open the screenshot with the default application:
$ xdg-open screenshot.png
The result may be a bit unexpected, depending on how your X display server is configured. We'll discuss that in the next exercise :)