Direct/system command for reading the display buffer of EV3 bricks?
I think you should be able to do this. The screen is represented by a framebuffer device. In Linux, everything is a file, so even the framebuffer is a file, namely /dev/fb0.

So, you should be able to read the display by reading the /dev/fb0 file using something like this:

EDIT: Using direct commands didn't work out so well, so here is a sample using system commands.

Getting file content
--------------------

Used to upload datalog files - file handle is only closed when reaching EOF
and file is not open for writing

BEGIN_GETFILE:

  Bytes send to the brick:

  xxxxxxxx0196xxxxxxx
  bbbbmmmmttssllllnnn...

  bbbb = Bytes in massage, mmmm = message counter, tt = type of command,
  ss = system command, llll = max bytes to read, nnnn.... = path


  Bytes send to the PC:

  xxxxxxxx039600xxxxxxxx00xxx
  bbbbmmmmttssrrllllllllhhppp...

  bbbb = bytes ion massage, mmmm = message counter, tt = type of command,
  ss = system command, rr = return status, llllllll = File size, hh = Handle,
  ppp... = payload


CONTINUE_GETFILE:

  Bytes send to the brick:

  0700xxxx019700xxxx
  bbbbmmmmttsshhllll

  bbbb = bytes in massage, mmmm = message counter, tt = type of command,
  ss = system command, hh = handle, llll = max bytes to read


  Bytes send to the PC:

  xxxxxxxx039700xxxxxxxx00xxx
  bbbbmmmmttssrrllllllllhhppp...

  bbbb = bytes in message, mmmm = message counter, tt = type of command,
  ss = system command, rr = return status, llllllll = File size,
  hh = Handle, ppp... = payload


Opcodes come from here System commands come from here. The size returned may be 0 since this is not a real file, but the framebuffer size is 7680 bytes (comes from here, i.e. (178+2)/3*128). Also, the maximum number of bytes allowable for max bytes to read depends on the connection type (USB, Bluetooth or Wi-Fi). 1000 should be a safe number.

Now, hopefully you have the data, but you still need to decode it. The framebuffer has an unusual format where there are 3 pixels per byte (see datasheet).

Assuming you have packed all of the data read into a single 7680 byte array you can use nested for loops to convert the data to whatever image format you like. For simplicity, I am going to use a .pbm. Just doing this in pseudo code...

// framebuffer data - 60 bytes/row * 128 rows
buffer[60*128]

// write the PBM header
write_line("P1")
write_line("178 128")
for (row = 0; row &lt; 128; row++) {
    // last byte in each row only has 1 pixel, so only loop first 59 bytes
    for (offset = 0; offset &lt; 59; offset++) {
        var b = buffer[row * 60 + offset];
        // 3 pixels per byte

        // bit 7 (or 6) is pixel 0
        write((b &amp; 128) ? "1 " : "0 ")
        // bit 4 (or 3) is pixel 1
        write((b &amp; 16) ? "1 " : "0 ")
        // bit 1 (or 0) is pixel 2
        write((b &amp; 2) ? "1 " : "0 ")
    }
    // the last pixel
    b = buffer[row * 60 + offset]
    write_line((b &amp; 128) ? "1" : "0")
}