├── .build.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── box.c ├── buffer.c ├── contrib └── completions │ ├── bash │ └── grim.bash │ ├── fish │ └── grim.fish │ └── meson.build ├── doc ├── grim.1.scd └── meson.build ├── flake.lock ├── flake.nix ├── include ├── box.h ├── buffer.h ├── grim.h ├── output-layout.h ├── render.h ├── write_jpg.h ├── write_png.h └── write_ppm.h ├── main.c ├── meson.build ├── meson_options.txt ├── output-layout.c ├── protocol ├── hyprland-toplevel-export-v1.xml ├── meson.build ├── wlr-foreign-toplevel-management-unstable-v1.xml └── wlr-screencopy-unstable-v1.xml ├── render.c ├── write_jpg.c ├── write_png.c └── write_ppm.c /.build.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - meson 4 | - wayland 5 | - wayland-protocols 6 | - pixman 7 | - libpng 8 | - libjpeg-turbo 9 | sources: 10 | - https://git.sr.ht/~emersion/grim 11 | tasks: 12 | - setup: | 13 | cd grim 14 | meson build 15 | - build: | 16 | cd grim 17 | ninja -C build 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.nix] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | /build 55 | .cache/ 56 | result* 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 emersion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grim-hyprland 2 | 3 | A fork of [grim] that takes advantage of [Hyprland]'s custom protocols to grab 4 | specific windows. 5 | 6 | ## Example usage 7 | 8 | Grab a screenshot from the focused window under Hyprland, using `hyprctl` and 9 | `jq`: 10 | 11 | ```sh 12 | grim -w "$(hyprctl activewindow -j | jq -r '.address')" 13 | ``` 14 | 15 | All original usages of Grim still work: 16 | 17 | Screenshoot all outputs: 18 | 19 | ```sh 20 | grim 21 | ``` 22 | 23 | Screenshoot a specific output: 24 | 25 | ```sh 26 | grim -o DP-1 27 | ``` 28 | 29 | Screenshoot a region: 30 | 31 | ```sh 32 | grim -g "10,20 300x400" 33 | ``` 34 | 35 | Select a region and screenshoot it: 36 | 37 | ```sh 38 | grim -g "$(slurp)" 39 | ``` 40 | 41 | Use a custom filename: 42 | 43 | ```sh 44 | grim $(xdg-user-dir PICTURES)/$(date +'%s_grim.png') 45 | ``` 46 | 47 | Screenshoot and copy to clipboard: 48 | 49 | ```sh 50 | grim - | wl-copy 51 | ``` 52 | 53 | Grab a screenshot from the focused monitor under Hyprland, using `hyprctl` and 54 | `jq`: 55 | 56 | ```sh 57 | grim -o "$(hyprctl monitors -j | jq -r '.[] | select(.focused) | .name')" 58 | ``` 59 | 60 | Pick a color, using ImageMagick: 61 | 62 | ```sh 63 | grim -g "$(slurp -p)" -t ppm - | convert - -format '%[pixel:p{0,0}]' txt:- 64 | ``` 65 | 66 | ## Building from source 67 | 68 | Install dependencies: 69 | 70 | * meson 71 | * wayland 72 | * pixman 73 | * libpng 74 | * libjpeg (optional) 75 | 76 | Then run: 77 | 78 | ```sh 79 | meson build 80 | ninja -C build 81 | ``` 82 | 83 | To run directly, use `build/grim`, or if you would like to do a system 84 | installation (in `/usr/local` by default), run `ninja -C build install`. 85 | 86 | ## Contributing 87 | 88 | This fork is on GitHub, you know what to do. 89 | 90 | ### Upstream contributions 91 | 92 | Report bugs on the [issue tracker], send patches on the [mailing list]. 93 | 94 | Join the IRC channel: [#emersion on Libera Chat]. 95 | 96 | ## License 97 | 98 | MIT 99 | 100 | [grim]: https://git.sr.ht/~emersion/grim 101 | [Hyprland]: https://github.com/hyprwm/Hyprland 102 | [slurp]: https://github.com/emersion/slurp 103 | [issue tracker]: https://todo.sr.ht/~emersion/grim 104 | [mailing list]: https://lists.sr.ht/~emersion/grim-dev 105 | [#emersion on Libera Chat]: ircs://irc.libera.chat/#emersion 106 | -------------------------------------------------------------------------------- /box.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "box.h" 5 | 6 | #include 7 | 8 | bool parse_box(struct grim_box *box, const char *str) { 9 | char *end = NULL; 10 | box->x = strtol(str, &end, 10); 11 | if (end[0] != ',') { 12 | return false; 13 | } 14 | 15 | char *next = end + 1; 16 | box->y = strtol(next, &end, 10); 17 | if (end[0] != ' ') { 18 | return false; 19 | } 20 | 21 | next = end + 1; 22 | box->width = strtol(next, &end, 10); 23 | if (end[0] != 'x') { 24 | return false; 25 | } 26 | 27 | next = end + 1; 28 | box->height = strtol(next, &end, 10); 29 | if (end[0] != '\0') { 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | bool is_empty_box(struct grim_box *box) { 37 | return box->width <= 0 || box->height <= 0; 38 | } 39 | 40 | bool intersect_box(struct grim_box *a, struct grim_box *b) { 41 | if (is_empty_box(a) || is_empty_box(b)) { 42 | return false; 43 | } 44 | 45 | int x1 = fmax(a->x, b->x); 46 | int y1 = fmax(a->y, b->y); 47 | int x2 = fmin(a->x + a->width, b->x + b->width); 48 | int y2 = fmin(a->y + a->height, b->y + b->height); 49 | 50 | struct grim_box box = { 51 | .x = x1, 52 | .y = y1, 53 | .width = x2 - x1, 54 | .height = y2 - y1, 55 | }; 56 | return !is_empty_box(&box); 57 | } 58 | -------------------------------------------------------------------------------- /buffer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "buffer.h" 11 | 12 | static void randname(char *buf) { 13 | struct timespec ts; 14 | clock_gettime(CLOCK_REALTIME, &ts); 15 | long r = ts.tv_nsec; 16 | for (int i = 0; i < 6; ++i) { 17 | buf[i] = 'A'+(r&15)+(r&16)*2; 18 | r >>= 5; 19 | } 20 | } 21 | 22 | static int anonymous_shm_open(void) { 23 | char name[] = "/grim-XXXXXX"; 24 | int retries = 100; 25 | 26 | do { 27 | randname(name + strlen(name) - 6); 28 | 29 | --retries; 30 | // shm_open guarantees that O_CLOEXEC is set 31 | int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); 32 | if (fd >= 0) { 33 | shm_unlink(name); 34 | return fd; 35 | } 36 | } while (retries > 0 && errno == EEXIST); 37 | 38 | return -1; 39 | } 40 | 41 | static int create_shm_file(off_t size) { 42 | int fd = anonymous_shm_open(); 43 | if (fd < 0) { 44 | return fd; 45 | } 46 | 47 | if (ftruncate(fd, size) < 0) { 48 | close(fd); 49 | return -1; 50 | } 51 | 52 | return fd; 53 | } 54 | 55 | struct grim_buffer *create_buffer(struct wl_shm *shm, enum wl_shm_format format, 56 | int32_t width, int32_t height, int32_t stride) { 57 | size_t size = stride * height; 58 | 59 | int fd = create_shm_file(size); 60 | if (fd == -1) { 61 | return NULL; 62 | } 63 | 64 | void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 65 | if (data == MAP_FAILED) { 66 | close(fd); 67 | return NULL; 68 | } 69 | 70 | struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); 71 | struct wl_buffer *wl_buffer = 72 | wl_shm_pool_create_buffer(pool, 0, width, height, stride, format); 73 | wl_shm_pool_destroy(pool); 74 | 75 | close(fd); 76 | 77 | struct grim_buffer *buffer = calloc(1, sizeof(struct grim_buffer)); 78 | buffer->wl_buffer = wl_buffer; 79 | buffer->data = data; 80 | buffer->width = width; 81 | buffer->height = height; 82 | buffer->stride = stride; 83 | buffer->size = size; 84 | buffer->format = format; 85 | return buffer; 86 | } 87 | 88 | void destroy_buffer(struct grim_buffer *buffer) { 89 | if (buffer == NULL) { 90 | return; 91 | } 92 | munmap(buffer->data, buffer->size); 93 | wl_buffer_destroy(buffer->wl_buffer); 94 | free(buffer); 95 | } 96 | -------------------------------------------------------------------------------- /contrib/completions/bash/grim.bash: -------------------------------------------------------------------------------- 1 | _grim() { 2 | _init_completion || return 3 | 4 | local CUR PREV 5 | CUR="${COMP_WORDS[COMP_CWORD]}" 6 | PREV="${COMP_WORDS[COMP_CWORD-1]}" 7 | 8 | if [[ "$PREV" == "-t" ]]; then 9 | COMPREPLY=($(compgen -W "png ppm jpeg" -- "$CUR")) 10 | return 11 | elif [[ "$PREV" == "-o" ]]; then 12 | local OUTPUTS 13 | OUTPUTS="$(swaymsg -t get_outputs 2>/dev/null | \ 14 | jq -r '.[] | select(.active) | "\(.name)\t\(.make) \(.model)"' 2>/dev/null)" 15 | 16 | COMPREPLY=($(compgen -W "$OUTPUTS" -- "$CUR")) 17 | return 18 | fi 19 | 20 | if [[ "$CUR" == -* ]]; then 21 | COMPREPLY=($(compgen -W "-h -s -g -t -q -o -c" -- "$CUR")) 22 | return 23 | fi 24 | 25 | # fall back to completing filenames 26 | _filedir 27 | } 28 | 29 | complete -F _grim grim 30 | -------------------------------------------------------------------------------- /contrib/completions/fish/grim.fish: -------------------------------------------------------------------------------- 1 | function complete_outputs 2 | if string length -q "$SWAYSOCK"; and command -sq jq 3 | swaymsg -t get_outputs | jq -r '.[] | select(.active) | "\(.name)\t\(.make) \(.model)"' 4 | else 5 | return 1 6 | end 7 | end 8 | 9 | complete -c grim -s t --exclusive --arguments 'png ppm jpeg' -d 'Output image format' 10 | complete -c grim -s q --exclusive -d 'Output jpeg quality (default 80)' 11 | complete -c grim -s g --exclusive -d 'Region to capture: , x' 12 | complete -c grim -s s --exclusive -d 'Output image scale factor' 13 | complete -c grim -s c -d 'Include cursors in the screenshot' 14 | complete -c grim -s h -d 'Show help and exit' 15 | complete -c grim -s o --exclusive --arguments '(complete_outputs)' -d 'Output name to capture' 16 | -------------------------------------------------------------------------------- /contrib/completions/meson.build: -------------------------------------------------------------------------------- 1 | if get_option('fish-completions') 2 | fish_files = files('fish/grim.fish') 3 | 4 | fish_comp = dependency('fish', required: false) 5 | if fish_comp.found() 6 | fish_install_dir = fish_comp.get_variable('completionsdir') 7 | else 8 | datadir = get_option('datadir') 9 | fish_install_dir = join_paths(datadir, 'fish', 'vendor_completions.d') 10 | endif 11 | install_data(fish_files, install_dir: fish_install_dir) 12 | endif 13 | 14 | 15 | if get_option('bash-completions') 16 | bash_comp = dependency('bash-completion') 17 | bash_files = files('bash/grim.bash') 18 | bash_install_dir = bash_comp.get_variable('completionsdir') 19 | install_data(bash_files, install_dir: bash_install_dir) 20 | endif 21 | -------------------------------------------------------------------------------- /doc/grim.1.scd: -------------------------------------------------------------------------------- 1 | grim(1) 2 | 3 | # NAME 4 | 5 | grim-hyprland - grab images from a Wayland compositor 6 | 7 | # SYNOPSIS 8 | 9 | *grim* [options...] [output-file] 10 | 11 | # DESCRIPTION 12 | 13 | grim is a command-line utility to take screenshots of Wayland desktops. For now 14 | it requires support for the screencopy protocol to work. Support for the 15 | xdg-output protocol is optional, but improves fractional scaling support. 16 | Window-specific screenshots require support for Hyprland's toplevel export 17 | protocol. 18 | 19 | grim will write an image to _output-file_, or to a timestamped file name in 20 | *$GRIM_DEFAULT_DIR* if not specified. If *$GRIM_DEFAULT_DIR* is not set, it 21 | falls back first to *$XDG_PICTURES_DIR* and then to the current working 22 | directory. If _output-file_ is *-*, grim will write the image to the standard 23 | output instead. 24 | 25 | # OPTIONS 26 | 27 | *-h* 28 | Show help message and quit. 29 | 30 | *-w*
31 | Set the window to capture. Incompatible with '-g' and '-o'. Requires 32 | compositor to implement *hyprland-toplevel-export-v1*. 33 | 34 | *-s* 35 | Set the output image's scale factor to _factor_. By default, the scale 36 | factor is set to the highest of all outputs. 37 | 38 | *-g* ", x" 39 | Set the region to capture, in layout coordinates. 40 | 41 | If set to *-*, read the region from the standard input instead. 42 | 43 | *-t* 44 | Set the output image's file format to _type_. By default, the filetype 45 | is set to *png*, valid values are *png*, *jpeg* or *ppm*. 46 | 47 | *-q* 48 | Set the output jpeg's filetype compression rate to _quality_. By default, 49 | the jpeg quality is *80*, valid values are between 0-100. 50 | 51 | *-l* 52 | Set the output PNG's filetype compression level to _level_. By default, 53 | the PNG compression level is 6 on a scale from 0 to 9. Level 9 gives 54 | the highest compression ratio, but may be slow; level 1 gives a lower 55 | compression ratio, but is faster. Level 0 does no compression at all, 56 | and produces very large files; it can be useful when grim is used 57 | in a pipeline with other commands. 58 | 59 | *-o* 60 | Set the output name to capture. 61 | 62 | *-c* 63 | Include cursors in the screenshot. 64 | 65 | # AUTHORS 66 | 67 | Maintained by Simon Ser , who is assisted by other 68 | open-source contributors. For more information about grim development, see 69 | . 70 | -------------------------------------------------------------------------------- /doc/meson.build: -------------------------------------------------------------------------------- 1 | scdoc = find_program('scdoc', required: get_option('man-pages')) 2 | if not scdoc.found() 3 | subdir_done() 4 | endif 5 | 6 | man_pages = ['grim.1.scd'] 7 | 8 | foreach src : man_pages 9 | topic = src.split('.')[0] 10 | section = src.split('.')[1] 11 | output = '@0@.@1@'.format(topic, section) 12 | 13 | custom_target( 14 | output, 15 | input: src, 16 | output: output, 17 | command: scdoc, 18 | feed: true, 19 | capture: true, 20 | install: true, 21 | install_dir: '@0@/man@1@'.format(get_option('mandir'), section), 22 | ) 23 | endforeach 24 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1727802920, 6 | "narHash": "sha256-HP89HZOT0ReIbI7IJZJQoJgxvB2Tn28V6XS3MNKnfLs=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "27e30d177e57d912d614c88c622dcfdb2e6e6515", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "systems": "systems" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1689347949, 28 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 29 | "owner": "nix-systems", 30 | "repo": "default-linux", 31 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default-linux", 37 | "type": "github" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Grab images from a Wayland compositor (Hyprland fork)"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | systems.url = "github:nix-systems/default-linux"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | systems, 13 | ... 14 | }: let 15 | inherit (nixpkgs) lib; 16 | forSystems = f: 17 | lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); 18 | in { 19 | overlays = { 20 | default = _: prev: rec { 21 | grim = prev.grim.overrideAttrs (old: { 22 | pname = "grim-hyprland"; 23 | version = self.rev or "dirty"; 24 | src = lib.cleanSource ./.; 25 | patches = []; 26 | buildInputs = 27 | old.buildInputs ++ [prev.wayland-scanner]; 28 | }); 29 | 30 | grim-hyprland = grim; 31 | }; 32 | }; 33 | 34 | packages = forSystems (pkgs: { 35 | inherit (self.overlays.default pkgs pkgs) grim grim-hyprland; 36 | default = self.packages.${pkgs.stdenv.system}.grim; 37 | }); 38 | 39 | devShells = forSystems (pkgs: { 40 | default = pkgs.mkShell { 41 | inputsFrom = [self.packages.${pkgs.stdenv.system}.grim]; 42 | packages = [pkgs.clang-tools]; 43 | }; 44 | }); 45 | 46 | formatter = forSystems (pkgs: pkgs.alejandra); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /include/box.h: -------------------------------------------------------------------------------- 1 | #ifndef _BOX_H 2 | #define _BOX_H 3 | 4 | #include 5 | #include 6 | 7 | struct grim_box { 8 | int32_t x, y; 9 | int32_t width, height; 10 | }; 11 | 12 | bool parse_box(struct grim_box *box, const char *str); 13 | bool is_empty_box(struct grim_box *box); 14 | bool intersect_box(struct grim_box *a, struct grim_box *b); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /include/buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef _BUFFER_H 2 | #define _BUFFER_H 3 | 4 | #include 5 | 6 | struct grim_buffer { 7 | struct wl_buffer *wl_buffer; 8 | void *data; 9 | int32_t width, height, stride; 10 | size_t size; 11 | enum wl_shm_format format; 12 | }; 13 | 14 | struct grim_buffer *create_buffer(struct wl_shm *shm, enum wl_shm_format format, 15 | int32_t width, int32_t height, int32_t stride); 16 | void destroy_buffer(struct grim_buffer *buffer); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /include/grim.h: -------------------------------------------------------------------------------- 1 | #ifndef _GRIM_H 2 | #define _GRIM_H 3 | 4 | #include 5 | 6 | #include "box.h" 7 | 8 | enum grim_filetype { 9 | GRIM_FILETYPE_PNG, 10 | GRIM_FILETYPE_PPM, 11 | GRIM_FILETYPE_JPEG, 12 | }; 13 | 14 | struct grim_state { 15 | struct wl_display *display; 16 | struct wl_registry *registry; 17 | struct wl_shm *shm; 18 | struct zxdg_output_manager_v1 *xdg_output_manager; 19 | struct wl_list outputs; 20 | 21 | bool use_win; 22 | 23 | union { 24 | struct zwlr_screencopy_manager_v1 *screencopy_manager; 25 | struct hyprland_toplevel_export_manager_v1 *toplevel_export_manager; 26 | }; 27 | 28 | size_t n_done; 29 | }; 30 | 31 | struct grim_buffer; 32 | 33 | struct grim_output { 34 | struct grim_state *state; 35 | struct wl_output *wl_output; 36 | struct zxdg_output_v1 *xdg_output; 37 | struct wl_list link; 38 | 39 | struct grim_box geometry; 40 | enum wl_output_transform transform; 41 | int32_t scale; 42 | 43 | struct grim_box logical_geometry; 44 | double logical_scale; // guessed from the logical size 45 | char *name; 46 | 47 | struct grim_buffer *buffer; 48 | 49 | union { 50 | struct zwlr_screencopy_frame_v1 *screencopy_frame; 51 | struct hyprland_toplevel_export_frame_v1 *toplevel_export_frame; 52 | }; 53 | 54 | union { 55 | uint32_t screencopy_frame_flags; // enum zwlr_screencopy_frame_v1_flags 56 | uint32_t toplevel_export_frame_flags; // enum hyprland_toplevel_export_frame_v1_flags 57 | }; 58 | 59 | }; 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /include/output-layout.h: -------------------------------------------------------------------------------- 1 | #ifndef _OUTPUT_LAYOUT_H 2 | #define _OUTPUT_LAYOUT_H 3 | 4 | #include 5 | 6 | #include "grim.h" 7 | 8 | void get_output_layout_extents(struct grim_state *state, struct grim_box *box); 9 | void apply_output_transform(enum wl_output_transform transform, 10 | int32_t *width, int32_t *height); 11 | double get_output_rotation(enum wl_output_transform transform); 12 | int get_output_flipped(enum wl_output_transform transform); 13 | void guess_output_logical_geometry(struct grim_output *output); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /include/render.h: -------------------------------------------------------------------------------- 1 | #ifndef _RENDER_H 2 | #define _RENDER_H 3 | 4 | #include 5 | 6 | #include "grim.h" 7 | 8 | pixman_image_t *render(struct grim_state *state, struct grim_box *geometry, 9 | double scale); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /include/write_jpg.h: -------------------------------------------------------------------------------- 1 | #ifndef _WRITE_JPEG_H 2 | #define _WRITE_JPEG_H 3 | 4 | #include 5 | #include 6 | 7 | int write_to_jpeg_stream(pixman_image_t *image, FILE *stream, int quality); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /include/write_png.h: -------------------------------------------------------------------------------- 1 | #ifndef _WRITE_PNG_H 2 | #define _WRITE_PNG_H 3 | 4 | #include 5 | #include 6 | 7 | int write_to_png_stream(pixman_image_t *image, FILE *stream, int comp_level); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /include/write_ppm.h: -------------------------------------------------------------------------------- 1 | #ifndef _WRITE_PPM_H 2 | #define _WRITE_PPM_H 3 | 4 | #include 5 | #include 6 | 7 | int write_to_ppm_stream(pixman_image_t *image, FILE *stream); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "buffer.h" 14 | #include "grim.h" 15 | #include "output-layout.h" 16 | #include "render.h" 17 | #include "write_ppm.h" 18 | #if HAVE_JPEG 19 | #include "write_jpg.h" 20 | #endif 21 | #include "write_png.h" 22 | 23 | #include "wlr-screencopy-unstable-v1-protocol.h" 24 | #include "xdg-output-unstable-v1-protocol.h" 25 | #include "hyprland-toplevel-export-v1-protocol.h" 26 | 27 | static void toplevel_export_frame_handle_buffer(void *data, 28 | struct hyprland_toplevel_export_frame_v1 *frame, uint32_t format, uint32_t width, 29 | uint32_t height, uint32_t stride) { 30 | struct grim_output *output = data; 31 | 32 | output->buffer = 33 | create_buffer(output->state->shm, format, width, height, stride); 34 | if (output->buffer == NULL) { 35 | fprintf(stderr, "failed to create buffer\n"); 36 | exit(EXIT_FAILURE); 37 | } 38 | 39 | output->geometry.width = width; 40 | output->geometry.height = height; 41 | 42 | guess_output_logical_geometry(output); 43 | } 44 | 45 | static void toplevel_export_frame_handle_damage(void *data, 46 | struct hyprland_toplevel_export_frame_v1 *frame, uint32_t x, uint32_t y, 47 | uint32_t width, uint32_t height) { 48 | // No-op 49 | } 50 | 51 | static void toplevel_export_frame_handle_flags(void *data, 52 | struct hyprland_toplevel_export_frame_v1 *frame, uint32_t flags) { 53 | struct grim_output *output = data; 54 | output->toplevel_export_frame_flags = flags; 55 | } 56 | 57 | static void toplevel_export_frame_handle_ready(void *data, 58 | struct hyprland_toplevel_export_frame_v1 *frame, uint32_t tv_sec_hi, 59 | uint32_t tv_sec_lo, uint32_t tv_nsec) { 60 | struct grim_output *output = data; 61 | ++output->state->n_done; 62 | } 63 | 64 | static void toplevel_export_frame_handle_failed(void *data, 65 | struct hyprland_toplevel_export_frame_v1 *frame) { 66 | fprintf(stderr, "failed to copy window\n"); 67 | exit(EXIT_FAILURE); 68 | } 69 | 70 | static void toplevel_export_frame_handle_linux_dmabuf(void *data, 71 | struct hyprland_toplevel_export_frame_v1 *frame, uint32_t format, 72 | uint32_t width, uint32_t height) { 73 | // No-op 74 | } 75 | 76 | static void toplevel_export_frame_handle_buffer_done(void *data, 77 | struct hyprland_toplevel_export_frame_v1 *frame) { 78 | struct grim_output *output = data; 79 | hyprland_toplevel_export_frame_v1_copy(frame, output->buffer->wl_buffer, 1); 80 | } 81 | 82 | static const struct hyprland_toplevel_export_frame_v1_listener toplevel_export_frame_listener = { 83 | .buffer = toplevel_export_frame_handle_buffer, 84 | .damage = toplevel_export_frame_handle_damage, 85 | .flags = toplevel_export_frame_handle_flags, 86 | .ready = toplevel_export_frame_handle_ready, 87 | .failed = toplevel_export_frame_handle_failed, 88 | .linux_dmabuf = toplevel_export_frame_handle_linux_dmabuf, 89 | .buffer_done = toplevel_export_frame_handle_buffer_done, 90 | }; 91 | 92 | static void screencopy_frame_handle_buffer(void *data, 93 | struct zwlr_screencopy_frame_v1 *frame, uint32_t format, uint32_t width, 94 | uint32_t height, uint32_t stride) { 95 | struct grim_output *output = data; 96 | 97 | output->buffer = 98 | create_buffer(output->state->shm, format, width, height, stride); 99 | if (output->buffer == NULL) { 100 | fprintf(stderr, "failed to create buffer\n"); 101 | exit(EXIT_FAILURE); 102 | } 103 | 104 | zwlr_screencopy_frame_v1_copy(frame, output->buffer->wl_buffer); 105 | } 106 | 107 | static void screencopy_frame_handle_flags(void *data, 108 | struct zwlr_screencopy_frame_v1 *frame, uint32_t flags) { 109 | struct grim_output *output = data; 110 | output->screencopy_frame_flags = flags; 111 | } 112 | 113 | static void screencopy_frame_handle_ready(void *data, 114 | struct zwlr_screencopy_frame_v1 *frame, uint32_t tv_sec_hi, 115 | uint32_t tv_sec_lo, uint32_t tv_nsec) { 116 | struct grim_output *output = data; 117 | ++output->state->n_done; 118 | } 119 | 120 | static void screencopy_frame_handle_failed(void *data, 121 | struct zwlr_screencopy_frame_v1 *frame) { 122 | struct grim_output *output = data; 123 | fprintf(stderr, "failed to copy output %s\n", output->name); 124 | exit(EXIT_FAILURE); 125 | } 126 | 127 | static const struct zwlr_screencopy_frame_v1_listener screencopy_frame_listener = { 128 | .buffer = screencopy_frame_handle_buffer, 129 | .flags = screencopy_frame_handle_flags, 130 | .ready = screencopy_frame_handle_ready, 131 | .failed = screencopy_frame_handle_failed, 132 | }; 133 | 134 | 135 | static void xdg_output_handle_logical_position(void *data, 136 | struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { 137 | struct grim_output *output = data; 138 | 139 | output->logical_geometry.x = x; 140 | output->logical_geometry.y = y; 141 | } 142 | 143 | static void xdg_output_handle_logical_size(void *data, 144 | struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { 145 | struct grim_output *output = data; 146 | 147 | output->logical_geometry.width = width; 148 | output->logical_geometry.height = height; 149 | } 150 | 151 | static void xdg_output_handle_done(void *data, 152 | struct zxdg_output_v1 *xdg_output) { 153 | struct grim_output *output = data; 154 | 155 | // Guess the output scale from the logical size 156 | int32_t width = output->geometry.width; 157 | int32_t height = output->geometry.height; 158 | apply_output_transform(output->transform, &width, &height); 159 | output->logical_scale = (double)width / output->logical_geometry.width; 160 | } 161 | 162 | static void xdg_output_handle_name(void *data, 163 | struct zxdg_output_v1 *xdg_output, const char *name) { 164 | struct grim_output *output = data; 165 | output->name = strdup(name); 166 | } 167 | 168 | static void xdg_output_handle_description(void *data, 169 | struct zxdg_output_v1 *xdg_output, const char *name) { 170 | // No-op 171 | } 172 | 173 | static const struct zxdg_output_v1_listener xdg_output_listener = { 174 | .logical_position = xdg_output_handle_logical_position, 175 | .logical_size = xdg_output_handle_logical_size, 176 | .done = xdg_output_handle_done, 177 | .name = xdg_output_handle_name, 178 | .description = xdg_output_handle_description, 179 | }; 180 | 181 | 182 | static void output_handle_geometry(void *data, struct wl_output *wl_output, 183 | int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, 184 | int32_t subpixel, const char *make, const char *model, 185 | int32_t transform) { 186 | struct grim_output *output = data; 187 | 188 | output->geometry.x = x; 189 | output->geometry.y = y; 190 | output->transform = transform; 191 | } 192 | 193 | static void output_handle_mode(void *data, struct wl_output *wl_output, 194 | uint32_t flags, int32_t width, int32_t height, int32_t refresh) { 195 | struct grim_output *output = data; 196 | 197 | if ((flags & WL_OUTPUT_MODE_CURRENT) != 0) { 198 | output->geometry.width = width; 199 | output->geometry.height = height; 200 | } 201 | } 202 | 203 | static void output_handle_done(void *data, struct wl_output *wl_output) { 204 | // No-op 205 | } 206 | 207 | static void output_handle_scale(void *data, struct wl_output *wl_output, 208 | int32_t factor) { 209 | struct grim_output *output = data; 210 | output->scale = factor; 211 | } 212 | 213 | static const struct wl_output_listener output_listener = { 214 | .geometry = output_handle_geometry, 215 | .mode = output_handle_mode, 216 | .done = output_handle_done, 217 | .scale = output_handle_scale, 218 | }; 219 | 220 | 221 | static void handle_global(void *data, struct wl_registry *registry, 222 | uint32_t name, const char *interface, uint32_t version) { 223 | struct grim_state *state = data; 224 | 225 | if (strcmp(interface, wl_shm_interface.name) == 0) { 226 | state->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); 227 | } else if (state->use_win) { 228 | if (strcmp(interface, hyprland_toplevel_export_manager_v1_interface.name) == 0) { 229 | state->toplevel_export_manager = wl_registry_bind(registry, name, 230 | &hyprland_toplevel_export_manager_v1_interface, 2); 231 | } 232 | } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { 233 | uint32_t bind_version = (version > 2) ? 2 : version; 234 | state->xdg_output_manager = wl_registry_bind(registry, name, 235 | &zxdg_output_manager_v1_interface, bind_version); 236 | } else if (strcmp(interface, wl_output_interface.name) == 0) { 237 | struct grim_output *output = calloc(1, sizeof(struct grim_output)); 238 | output->state = state; 239 | output->scale = 1; 240 | output->wl_output = wl_registry_bind(registry, name, 241 | &wl_output_interface, 3); 242 | wl_output_add_listener(output->wl_output, &output_listener, output); 243 | wl_list_insert(&state->outputs, &output->link); 244 | } else if (strcmp(interface, zwlr_screencopy_manager_v1_interface.name) == 0) { 245 | state->screencopy_manager = wl_registry_bind(registry, name, 246 | &zwlr_screencopy_manager_v1_interface, 1); 247 | } 248 | } 249 | 250 | static void handle_global_remove(void *data, struct wl_registry *registry, 251 | uint32_t name) { 252 | // who cares 253 | } 254 | 255 | static const struct wl_registry_listener registry_listener = { 256 | .global = handle_global, 257 | .global_remove = handle_global_remove, 258 | }; 259 | 260 | static bool default_filename(char *filename, size_t n, int filetype) { 261 | time_t time_epoch = time(NULL); 262 | struct tm *time = localtime(&time_epoch); 263 | if (time == NULL) { 264 | perror("localtime"); 265 | return false; 266 | } 267 | 268 | char *format_str; 269 | const char *ext = NULL; 270 | switch (filetype) { 271 | case GRIM_FILETYPE_PNG: 272 | ext = "png"; 273 | break; 274 | case GRIM_FILETYPE_PPM: 275 | ext = "ppm"; 276 | break; 277 | case GRIM_FILETYPE_JPEG: 278 | #if HAVE_JPEG 279 | ext = "jpeg"; 280 | break; 281 | #else 282 | abort(); 283 | #endif 284 | } 285 | assert(ext != NULL); 286 | char tmpstr[32]; 287 | sprintf(tmpstr, "%%Y%%m%%d_%%Hh%%Mm%%Ss_grim.%s", ext); 288 | format_str = tmpstr; 289 | if (strftime(filename, n, format_str, time) == 0) { 290 | fprintf(stderr, "failed to format datetime with strftime(3)\n"); 291 | return false; 292 | } 293 | return true; 294 | } 295 | 296 | static bool path_exists(const char *path) { 297 | return path && access(path, R_OK) != -1; 298 | } 299 | 300 | char *get_xdg_pictures_dir(void) { 301 | const char *home_dir = getenv("HOME"); 302 | if (home_dir == NULL) { 303 | return NULL; 304 | } 305 | 306 | char *config_file; 307 | const char user_dirs_file[] = "user-dirs.dirs"; 308 | const char config_home_fallback[] = ".config"; 309 | const char *config_home = getenv("XDG_CONFIG_HOME"); 310 | if (config_home == NULL || config_home[0] == 0) { 311 | size_t size = strlen(home_dir) + strlen("/") + 312 | strlen(config_home_fallback) + strlen("/") + strlen(user_dirs_file) + 1; 313 | config_file = malloc(size); 314 | if (config_file == NULL) { 315 | return NULL; 316 | } 317 | snprintf(config_file, size, "%s/%s/%s", home_dir, config_home_fallback, user_dirs_file); 318 | } else { 319 | size_t size = strlen(config_home) + strlen("/") + strlen(user_dirs_file) + 1; 320 | config_file = malloc(size); 321 | if (config_file == NULL) { 322 | return NULL; 323 | } 324 | snprintf(config_file, size, "%s/%s", config_home, user_dirs_file); 325 | } 326 | 327 | FILE *file = fopen(config_file, "r"); 328 | free(config_file); 329 | if (file == NULL) { 330 | return NULL; 331 | } 332 | 333 | char *line = NULL; 334 | size_t line_size = 0; 335 | ssize_t nread; 336 | char *pictures_dir = NULL; 337 | while ((nread = getline(&line, &line_size, file)) != -1) { 338 | if (nread > 0 && line[nread - 1] == '\n') { 339 | line[nread - 1] = '\0'; 340 | } 341 | 342 | if (strlen(line) == 0 || line[0] == '#') { 343 | continue; 344 | } 345 | 346 | size_t i = 0; 347 | while (line[i] == ' ') { 348 | i++; 349 | } 350 | const char prefix[] = "XDG_PICTURES_DIR="; 351 | if (strncmp(&line[i], prefix, strlen(prefix)) == 0) { 352 | const char *line_remaining = &line[i] + strlen(prefix); 353 | wordexp_t p; 354 | if (wordexp(line_remaining, &p, WRDE_UNDEF) == 0) { 355 | free(pictures_dir); 356 | pictures_dir = strdup(p.we_wordv[0]); 357 | wordfree(&p); 358 | } 359 | } 360 | } 361 | free(line); 362 | fclose(file); 363 | return pictures_dir; 364 | } 365 | 366 | char *get_output_dir(void) { 367 | const char *grim_default_dir = getenv("GRIM_DEFAULT_DIR"); 368 | if (path_exists(grim_default_dir)) { 369 | return strdup(grim_default_dir); 370 | } 371 | 372 | char *xdg_fallback_dir = get_xdg_pictures_dir(); 373 | if (path_exists(xdg_fallback_dir)) { 374 | return xdg_fallback_dir; 375 | } else { 376 | free(xdg_fallback_dir); 377 | } 378 | 379 | return strdup("."); 380 | } 381 | 382 | static const char usage[] = 383 | "Usage: grim [options...] [output-file]\n" 384 | "\n" 385 | " -h Show help message and quit.\n" 386 | " -w
Set individual window to screenshot. (Hyprland only).\n" 387 | " -s Set the output image scale factor. Defaults to the\n" 388 | " greatest output scale factor.\n" 389 | " -g Set the region to capture.\n" 390 | " -t png|ppm|jpeg Set the output filetype. Defaults to png.\n" 391 | " -q Set the JPEG filetype quality 0-100. Defaults to 80.\n" 392 | " -l Set the PNG filetype compression level 0-9. Defaults to 6.\n" 393 | " -o Set the output name to capture.\n" 394 | " -c Include cursors in the screenshot.\n"; 395 | 396 | int main(int argc, char *argv[]) { 397 | bool use_win = false; 398 | int win_handle = 0; 399 | double scale = 1.0; 400 | bool use_greatest_scale = true; 401 | struct grim_box *geometry = NULL; 402 | char *geometry_output = NULL; 403 | enum grim_filetype output_filetype = GRIM_FILETYPE_PNG; 404 | int jpeg_quality = 80; 405 | int png_level = 6; // current default png/zlib compression level 406 | bool with_cursor = false; 407 | int opt; 408 | while ((opt = getopt(argc, argv, "hw:s:g:t:q:l:o:c")) != -1) { 409 | switch (opt) { 410 | case 'h': 411 | printf("%s", usage); 412 | return EXIT_SUCCESS; 413 | case 'w':; 414 | char *endptr = NULL; 415 | errno = 0; 416 | win_handle = strtol(optarg, &endptr, 16); 417 | if (*endptr != '\0' || errno) { 418 | fprintf(stderr, "expected hex window address\n"); 419 | return EXIT_FAILURE; 420 | } 421 | use_win = true; 422 | break; 423 | case 's': 424 | use_greatest_scale = false; 425 | scale = strtod(optarg, NULL); 426 | break; 427 | case 'g':; 428 | char *geometry_str = NULL; 429 | if (strcmp(optarg, "-") == 0) { 430 | size_t n = 0; 431 | ssize_t nread = getline(&geometry_str, &n, stdin); 432 | if (nread < 0) { 433 | free(geometry_str); 434 | fprintf(stderr, "failed to read a line from stdin\n"); 435 | return EXIT_FAILURE; 436 | } 437 | 438 | if (nread > 0 && geometry_str[nread - 1] == '\n') { 439 | geometry_str[nread - 1] = '\0'; 440 | } 441 | } else { 442 | geometry_str = strdup(optarg); 443 | } 444 | 445 | free(geometry); 446 | geometry = calloc(1, sizeof(struct grim_box)); 447 | if (!parse_box(geometry, geometry_str)) { 448 | fprintf(stderr, "invalid geometry\n"); 449 | return EXIT_FAILURE; 450 | } 451 | 452 | free(geometry_str); 453 | break; 454 | case 't': 455 | if (strcmp(optarg, "png") == 0) { 456 | output_filetype = GRIM_FILETYPE_PNG; 457 | } else if (strcmp(optarg, "ppm") == 0) { 458 | output_filetype = GRIM_FILETYPE_PPM; 459 | } else if (strcmp(optarg, "jpeg") == 0) { 460 | #if HAVE_JPEG 461 | output_filetype = GRIM_FILETYPE_JPEG; 462 | #else 463 | fprintf(stderr, "jpeg support disabled\n"); 464 | return EXIT_FAILURE; 465 | #endif 466 | } else { 467 | fprintf(stderr, "invalid filetype\n"); 468 | return EXIT_FAILURE; 469 | } 470 | break; 471 | case 'q': 472 | if (output_filetype != GRIM_FILETYPE_JPEG) { 473 | fprintf(stderr, "quality is used only for jpeg files\n"); 474 | return EXIT_FAILURE; 475 | } else { 476 | char *endptr = NULL; 477 | errno = 0; 478 | jpeg_quality = strtol(optarg, &endptr, 10); 479 | if (*endptr != '\0' || errno) { 480 | fprintf(stderr, "quality must be a integer\n"); 481 | return EXIT_FAILURE; 482 | } 483 | if (jpeg_quality < 0 || jpeg_quality > 100) { 484 | fprintf(stderr, "quality valid values are between 0-100\n"); 485 | return EXIT_FAILURE; 486 | } 487 | } 488 | break; 489 | case 'l': 490 | if (output_filetype != GRIM_FILETYPE_PNG) { 491 | fprintf(stderr, "compression level is used only for png files\n"); 492 | return EXIT_FAILURE; 493 | } else { 494 | char *endptr = NULL; 495 | errno = 0; 496 | png_level = strtol(optarg, &endptr, 10); 497 | if (*endptr != '\0' || errno) { 498 | fprintf(stderr, "level must be a integer\n"); 499 | return EXIT_FAILURE; 500 | } 501 | if (png_level < 0 || png_level > 9) { 502 | fprintf(stderr, "compression level valid values are between 0-9\n"); 503 | return EXIT_FAILURE; 504 | } 505 | } 506 | break; 507 | case 'o': 508 | free(geometry_output); 509 | geometry_output = strdup(optarg); 510 | break; 511 | case 'c': 512 | with_cursor = true; 513 | break; 514 | default: 515 | return EXIT_FAILURE; 516 | } 517 | } 518 | 519 | if (use_win && (geometry || geometry_output)) { 520 | fprintf(stderr, "-w is incompatible with -g and -o\n"); 521 | return EXIT_FAILURE; 522 | } 523 | 524 | const char *output_filename; 525 | char *output_filepath; 526 | char tmp[64]; 527 | if (optind >= argc) { 528 | if (!default_filename(tmp, sizeof(tmp), output_filetype)) { 529 | fprintf(stderr, "failed to generate default filename\n"); 530 | return EXIT_FAILURE; 531 | } 532 | output_filename = tmp; 533 | 534 | char *output_dir = get_output_dir(); 535 | int len = snprintf(NULL, 0, "%s/%s", output_dir, output_filename); 536 | if (len < 0) { 537 | perror("snprintf failed"); 538 | return EXIT_FAILURE; 539 | } 540 | output_filepath = malloc(len + 1); 541 | snprintf(output_filepath, len + 1, "%s/%s", output_dir, output_filename); 542 | free(output_dir); 543 | } else if (optind < argc - 1) { 544 | printf("%s", usage); 545 | return EXIT_FAILURE; 546 | } else { 547 | output_filename = argv[optind]; 548 | output_filepath = strdup(output_filename); 549 | } 550 | 551 | struct grim_state state = {0}; 552 | state.use_win = use_win; 553 | wl_list_init(&state.outputs); 554 | 555 | state.display = wl_display_connect(NULL); 556 | if (state.display == NULL) { 557 | fprintf(stderr, "failed to create display\n"); 558 | return EXIT_FAILURE; 559 | } 560 | 561 | state.registry = wl_display_get_registry(state.display); 562 | wl_registry_add_listener(state.registry, ®istry_listener, &state); 563 | if (wl_display_roundtrip(state.display) < 0) { 564 | fprintf(stderr, "wl_display_roundtrip() failed\n"); 565 | return EXIT_FAILURE; 566 | } 567 | 568 | if (state.shm == NULL) { 569 | fprintf(stderr, "compositor doesn't support wl_shm\n"); 570 | return EXIT_FAILURE; 571 | } 572 | if (state.screencopy_manager == NULL) { 573 | fprintf(stderr, "compositor doesn't support wlr-screencopy-unstable-v1\n"); 574 | return EXIT_FAILURE; 575 | } 576 | 577 | size_t n_pending = 0; 578 | if (use_win) { 579 | if (state.toplevel_export_manager == NULL) { 580 | fprintf(stderr, "compositor doesn't support hyprland_toplevel_export_manager\n"); 581 | return EXIT_FAILURE; 582 | } 583 | 584 | struct grim_output *output = calloc(1, sizeof(struct grim_output)); 585 | output->state = &state; 586 | output->scale = 1; 587 | output->transform = WL_OUTPUT_TRANSFORM_NORMAL; 588 | wl_list_insert(&state.outputs, &output->link); 589 | 590 | output->toplevel_export_frame = 591 | hyprland_toplevel_export_manager_v1_capture_toplevel( 592 | state.toplevel_export_manager, with_cursor, win_handle); 593 | 594 | hyprland_toplevel_export_frame_v1_add_listener( 595 | output->toplevel_export_frame, &toplevel_export_frame_listener, output); 596 | 597 | n_pending = 1; 598 | } else { 599 | if (wl_list_empty(&state.outputs)) { 600 | fprintf(stderr, "no wl_output\n"); 601 | return EXIT_FAILURE; 602 | } 603 | 604 | if (state.xdg_output_manager != NULL) { 605 | struct grim_output *output; 606 | wl_list_for_each(output, &state.outputs, link) { 607 | output->xdg_output = zxdg_output_manager_v1_get_xdg_output( 608 | state.xdg_output_manager, output->wl_output); 609 | zxdg_output_v1_add_listener(output->xdg_output, 610 | &xdg_output_listener, output); 611 | } 612 | 613 | if (wl_display_roundtrip(state.display) < 0) { 614 | fprintf(stderr, "wl_display_roundtrip() failed\n"); 615 | return EXIT_FAILURE; 616 | } 617 | } else { 618 | fprintf(stderr, "warning: zxdg_output_manager_v1 isn't available, " 619 | "guessing the output layout\n"); 620 | 621 | struct grim_output *output; 622 | wl_list_for_each(output, &state.outputs, link) { 623 | guess_output_logical_geometry(output); 624 | } 625 | } 626 | 627 | if (geometry_output != NULL) { 628 | struct grim_output *output; 629 | wl_list_for_each(output, &state.outputs, link) { 630 | if (output->name != NULL && 631 | strcmp(output->name, geometry_output) == 0) { 632 | geometry = calloc(1, sizeof(struct grim_box)); 633 | memcpy(geometry, &output->logical_geometry, 634 | sizeof(struct grim_box)); 635 | } 636 | } 637 | 638 | if (geometry == NULL) { 639 | fprintf(stderr, "unknown output '%s'\n", geometry_output); 640 | return EXIT_FAILURE; 641 | } 642 | } 643 | 644 | struct grim_output* output; 645 | wl_list_for_each(output, &state.outputs, link) { 646 | if (geometry != NULL && 647 | !intersect_box(geometry, &output->logical_geometry)) { 648 | continue; 649 | } 650 | if (use_greatest_scale && output->logical_scale > scale) { 651 | scale = output->logical_scale; 652 | } 653 | 654 | output->screencopy_frame = zwlr_screencopy_manager_v1_capture_output( 655 | state.screencopy_manager, with_cursor, output->wl_output); 656 | zwlr_screencopy_frame_v1_add_listener(output->screencopy_frame, 657 | &screencopy_frame_listener, output); 658 | 659 | ++n_pending; 660 | } 661 | } 662 | 663 | if (n_pending == 0) { 664 | fprintf(stderr, "supplied geometry did not intersect with any outputs\n"); 665 | return EXIT_FAILURE; 666 | } 667 | 668 | bool done = false; 669 | while (!done && wl_display_dispatch(state.display) != -1) { 670 | done = (state.n_done == n_pending); 671 | } 672 | if (!done) { 673 | fprintf(stderr, "failed to screenshoot all outputs\n"); 674 | return EXIT_FAILURE; 675 | } 676 | 677 | if (geometry == NULL) { 678 | geometry = calloc(1, sizeof(struct grim_box)); 679 | get_output_layout_extents(&state, geometry); 680 | } 681 | 682 | pixman_image_t *image = render(&state, geometry, scale); 683 | if (image == NULL) { 684 | return EXIT_FAILURE; 685 | } 686 | 687 | FILE *file; 688 | if (strcmp(output_filename, "-") == 0) { 689 | file = stdout; 690 | } else { 691 | file = fopen(output_filepath, "w"); 692 | if (!file) { 693 | fprintf(stderr, "Failed to open file '%s' for writing: %s\n", 694 | output_filepath, strerror(errno)); 695 | return EXIT_FAILURE; 696 | } 697 | } 698 | 699 | int ret = 0; 700 | switch (output_filetype) { 701 | case GRIM_FILETYPE_PPM: 702 | ret = write_to_ppm_stream(image, file); 703 | break; 704 | case GRIM_FILETYPE_PNG: 705 | ret = write_to_png_stream(image, file, png_level); 706 | break; 707 | case GRIM_FILETYPE_JPEG: 708 | #if HAVE_JPEG 709 | ret = write_to_jpeg_stream(image, file, jpeg_quality); 710 | break; 711 | #else 712 | abort(); 713 | #endif 714 | } 715 | if (ret == -1) { 716 | // Error messages will be printed at the source 717 | return EXIT_FAILURE; 718 | } 719 | 720 | if (strcmp(output_filename, "-") != 0) { 721 | fclose(file); 722 | } 723 | 724 | free(output_filepath); 725 | pixman_image_unref(image); 726 | 727 | struct grim_output *output; 728 | struct grim_output *output_tmp; 729 | wl_list_for_each_safe(output, output_tmp, &state.outputs, link) { 730 | wl_list_remove(&output->link); 731 | free(output->name); 732 | if (output->screencopy_frame != NULL) { 733 | zwlr_screencopy_frame_v1_destroy(output->screencopy_frame); 734 | } 735 | destroy_buffer(output->buffer); 736 | if (output->xdg_output != NULL) { 737 | zxdg_output_v1_destroy(output->xdg_output); 738 | } 739 | if (output->wl_output != NULL) { 740 | wl_output_release(output->wl_output); 741 | } 742 | free(output); 743 | } 744 | if (use_win) { 745 | hyprland_toplevel_export_manager_v1_destroy(state.toplevel_export_manager); 746 | } else { 747 | zwlr_screencopy_manager_v1_destroy(state.screencopy_manager); 748 | } 749 | if (state.xdg_output_manager != NULL) { 750 | zxdg_output_manager_v1_destroy(state.xdg_output_manager); 751 | } 752 | wl_shm_destroy(state.shm); 753 | wl_registry_destroy(state.registry); 754 | wl_display_disconnect(state.display); 755 | free(geometry); 756 | free(geometry_output); 757 | return EXIT_SUCCESS; 758 | } 759 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'grim', 3 | 'c', 4 | version: '1.4.1', 5 | license: 'MIT', 6 | meson_version: '>=0.59.0', 7 | default_options: ['c_std=c11', 'warning_level=3', 'werror=true'], 8 | ) 9 | 10 | cc = meson.get_compiler('c') 11 | 12 | add_project_arguments(cc.get_supported_arguments([ 13 | '-Wno-unused-parameter', 14 | '-Wundef', 15 | ]), language: 'c') 16 | 17 | png = dependency('libpng') 18 | jpeg = dependency('libjpeg', required: get_option('jpeg')) 19 | math = cc.find_library('m') 20 | pixman = dependency('pixman-1') 21 | realtime = cc.find_library('rt') 22 | wayland_client = dependency('wayland-client') 23 | 24 | is_le = host_machine.endian() == 'little' 25 | add_project_arguments([ 26 | '-D_POSIX_C_SOURCE=200809L', 27 | '-DGRIM_LITTLE_ENDIAN=@0@'.format(is_le.to_int()), 28 | '-DHAVE_JPEG=@0@'.format(jpeg.found().to_int()), 29 | ], language: 'c') 30 | 31 | subdir('contrib/completions') 32 | subdir('protocol') 33 | 34 | grim_files = [ 35 | 'box.c', 36 | 'buffer.c', 37 | 'main.c', 38 | 'output-layout.c', 39 | 'render.c', 40 | 'write_ppm.c', 41 | 'write_png.c', 42 | ] 43 | 44 | grim_deps = [ 45 | math, 46 | pixman, 47 | png, 48 | realtime, 49 | wayland_client, 50 | ] 51 | 52 | if jpeg.found() 53 | grim_files += ['write_jpg.c'] 54 | grim_deps += [jpeg] 55 | endif 56 | 57 | executable( 58 | 'grim', 59 | [files(grim_files), protocols_src], 60 | dependencies: grim_deps, 61 | include_directories: 'include', 62 | install: true, 63 | ) 64 | 65 | subdir('doc') 66 | 67 | summary({ 68 | 'JPEG': jpeg.found(), 69 | 'Manual pages': scdoc.found(), 70 | }, bool_yn: true) 71 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('jpeg', type: 'feature', value: 'auto', description: 'Enable JPEG support') 2 | option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') 3 | option('fish-completions', type: 'boolean', value: false, description: 'Install fish completions') 4 | option('bash-completions', type: 'boolean', value: false, description: 'Install bash completions') 5 | -------------------------------------------------------------------------------- /output-layout.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "output-layout.h" 5 | #include "grim.h" 6 | 7 | #ifndef M_PI 8 | #define M_PI 3.14159265358979323846 9 | #endif 10 | 11 | void get_output_layout_extents(struct grim_state *state, struct grim_box *box) { 12 | int32_t x1 = INT_MAX, y1 = INT_MAX; 13 | int32_t x2 = INT_MIN, y2 = INT_MIN; 14 | 15 | struct grim_output *output; 16 | wl_list_for_each(output, &state->outputs, link) { 17 | if (output->logical_geometry.x < x1) { 18 | x1 = output->logical_geometry.x; 19 | } 20 | if (output->logical_geometry.y < y1) { 21 | y1 = output->logical_geometry.y; 22 | } 23 | if (output->logical_geometry.x + output->logical_geometry.width > x2) { 24 | x2 = output->logical_geometry.x + output->logical_geometry.width; 25 | } 26 | if (output->logical_geometry.y + output->logical_geometry.height > y2) { 27 | y2 = output->logical_geometry.y + output->logical_geometry.height; 28 | } 29 | } 30 | 31 | box->x = x1; 32 | box->y = y1; 33 | box->width = x2 - x1; 34 | box->height = y2 - y1; 35 | } 36 | 37 | void apply_output_transform(enum wl_output_transform transform, 38 | int32_t *width, int32_t *height) { 39 | if (transform & WL_OUTPUT_TRANSFORM_90) { 40 | int32_t tmp = *width; 41 | *width = *height; 42 | *height = tmp; 43 | } 44 | } 45 | 46 | double get_output_rotation(enum wl_output_transform transform) { 47 | switch (transform & ~WL_OUTPUT_TRANSFORM_FLIPPED) { 48 | case WL_OUTPUT_TRANSFORM_90: 49 | return M_PI / 2; 50 | case WL_OUTPUT_TRANSFORM_180: 51 | return M_PI; 52 | case WL_OUTPUT_TRANSFORM_270: 53 | return 3 * M_PI / 2; 54 | } 55 | return 0; 56 | } 57 | 58 | int get_output_flipped(enum wl_output_transform transform) { 59 | return transform & WL_OUTPUT_TRANSFORM_FLIPPED ? -1 : 1; 60 | } 61 | 62 | void guess_output_logical_geometry(struct grim_output *output) { 63 | output->logical_geometry.x = output->geometry.x; 64 | output->logical_geometry.y = output->geometry.y; 65 | output->logical_geometry.width = output->geometry.width / output->scale; 66 | output->logical_geometry.height = output->geometry.height / output->scale; 67 | apply_output_transform(output->transform, 68 | &output->logical_geometry.width, 69 | &output->logical_geometry.height); 70 | output->logical_scale = output->scale; 71 | } 72 | -------------------------------------------------------------------------------- /protocol/hyprland-toplevel-export-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2022 Vaxry 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | 34 | This protocol allows clients to ask for exporting another toplevel's 35 | surface(s) to a buffer. 36 | 37 | Particularly useful for sharing a single window. 38 | 39 | 40 | 41 | 42 | This object is a manager which offers requests to start capturing from a 43 | source. 44 | 45 | 46 | 47 | 48 | Capture the next frame of a toplevel. (window) 49 | 50 | The captured frame will not contain any server-side decorations and will 51 | ignore the compositor-set geometry, like e.g. rounded corners. 52 | 53 | It will contain all the subsurfaces and popups, however the latter will be clipped 54 | to the geometry of the base surface. 55 | 56 | The handle parameter refers to the address of the window as seen in `hyprctl clients`. 57 | For example, for d161e7b0 it would be 3512854448. 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | All objects created by the manager will still remain valid, until their 68 | appropriate destroy request has been called. 69 | 70 | 71 | 72 | 73 | 74 | 75 | Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle. 76 | 77 | 78 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | This object represents a single frame. 88 | 89 | When created, a series of buffer events will be sent, each representing a 90 | supported buffer type. The "buffer_done" event is sent afterwards to 91 | indicate that all supported buffer types have been enumerated. The client 92 | will then be able to send a "copy" request. If the capture is successful, 93 | the compositor will send a "flags" followed by a "ready" event. 94 | 95 | wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. 96 | 97 | If the capture failed, the "failed" event is sent. This can happen anytime 98 | before the "ready" event. 99 | 100 | Once either a "ready" or a "failed" event is received, the client should 101 | destroy the frame. 102 | 103 | 104 | 105 | 106 | Provides information about wl_shm buffer parameters that need to be 107 | used for this frame. This event is sent once after the frame is created 108 | if wl_shm buffers are supported. 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Copy the frame to the supplied buffer. The buffer must have the 119 | correct size, see hyprland_toplevel_export_frame_v1.buffer and 120 | hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a 121 | supported format. 122 | 123 | If the frame is successfully copied, a "flags" and a "ready" event is 124 | sent. Otherwise, a "failed" event is sent. 125 | 126 | This event will wait for appropriate damage to be copied, unless the ignore_damage 127 | arg is set to a non-zero value. 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | This event is sent right before the ready event when ignore_damage was 136 | not set. It may be generated multiple times for each copy 137 | request. 138 | 139 | The arguments describe a box around an area that has changed since the 140 | last copy request that was derived from the current screencopy manager 141 | instance. 142 | 143 | The union of all regions received between the call to copy 144 | and a ready event is the total damage since the prior ready event. 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 155 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | Provides flags about the frame. This event is sent once before the 166 | "ready" event. 167 | 168 | 169 | 170 | 171 | 172 | 173 | Called as soon as the frame is copied, indicating it is available 174 | for reading. This event includes the time at which presentation happened 175 | at. 176 | 177 | The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, 178 | each component being an unsigned 32-bit value. Whole seconds are in 179 | tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, 180 | and the additional fractional part in tv_nsec as nanoseconds. Hence, 181 | for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part 182 | may have an arbitrary offset at start. 183 | 184 | After receiving this event, the client should destroy the object. 185 | 186 | 188 | 190 | 192 | 193 | 194 | 195 | 196 | This event indicates that the attempted frame copy has failed. 197 | 198 | After receiving this event, the client should destroy the object. 199 | 200 | 201 | 202 | 203 | 204 | Destroys the frame. This request can be sent at any time by the client. 205 | 206 | 207 | 208 | 209 | 210 | Provides information about linux-dmabuf buffer parameters that need to 211 | be used for this frame. This event is sent once after the frame is 212 | created if linux-dmabuf buffers are supported. 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | This event is sent once after all buffer events have been sent. 222 | 223 | The client should proceed to create a buffer of one of the supported 224 | types, and send a "copy" request. 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /protocol/meson.build: -------------------------------------------------------------------------------- 1 | wayland_protos = dependency('wayland-protocols', version: '>=1.14') 2 | wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') 3 | 4 | wayland_scanner = dependency('wayland-scanner', version: '>=1.14.91', native: true) 5 | wayland_scanner_path = wayland_scanner.get_variable(pkgconfig: 'wayland_scanner') 6 | wayland_scanner_prog = find_program(wayland_scanner_path, native: true) 7 | 8 | wayland_scanner_code = generator( 9 | wayland_scanner_prog, 10 | output: '@BASENAME@-protocol.c', 11 | arguments: ['private-code', '@INPUT@', '@OUTPUT@'], 12 | ) 13 | 14 | wayland_scanner_client = generator( 15 | wayland_scanner_prog, 16 | output: '@BASENAME@-protocol.h', 17 | arguments: ['client-header', '@INPUT@', '@OUTPUT@'], 18 | ) 19 | 20 | protocols = [ 21 | wl_protocol_dir / 'unstable/xdg-output/xdg-output-unstable-v1.xml', 22 | 'wlr-screencopy-unstable-v1.xml', 23 | 'wlr-foreign-toplevel-management-unstable-v1.xml', 24 | 'hyprland-toplevel-export-v1.xml', 25 | ] 26 | 27 | protocols_src = [] 28 | foreach xml : protocols 29 | protocols_src += wayland_scanner_code.process(xml) 30 | protocols_src += wayland_scanner_client.process(xml) 31 | endforeach 32 | -------------------------------------------------------------------------------- /protocol/wlr-foreign-toplevel-management-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Ilia Bozhinov 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | The purpose of this protocol is to enable the creation of taskbars 31 | and docks by providing them with a list of opened applications and 32 | letting them request certain actions on them, like maximizing, etc. 33 | 34 | After a client binds the zwlr_foreign_toplevel_manager_v1, each opened 35 | toplevel window will be sent via the toplevel event 36 | 37 | 38 | 39 | 40 | This event is emitted whenever a new toplevel window is created. It 41 | is emitted for all toplevels, regardless of the app that has created 42 | them. 43 | 44 | All initial details of the toplevel(title, app_id, states, etc.) will 45 | be sent immediately after this event via the corresponding events in 46 | zwlr_foreign_toplevel_handle_v1. 47 | 48 | 49 | 50 | 51 | 52 | 53 | Indicates the client no longer wishes to receive events for new toplevels. 54 | However the compositor may emit further toplevel_created events, until 55 | the finished event is emitted. 56 | 57 | The client must not send any more requests after this one. 58 | 59 | 60 | 61 | 62 | 63 | This event indicates that the compositor is done sending events to the 64 | zwlr_foreign_toplevel_manager_v1. The server will destroy the object 65 | immediately after sending this request, so it will become invalid and 66 | the client should free any resources associated with it. 67 | 68 | 69 | 70 | 71 | 72 | 73 | A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel 74 | window. Each app may have multiple opened toplevels. 75 | 76 | Each toplevel has a list of outputs it is visible on, conveyed to the 77 | client with the output_enter and output_leave events. 78 | 79 | 80 | 81 | 82 | This event is emitted whenever the title of the toplevel changes. 83 | 84 | 85 | 86 | 87 | 88 | 89 | This event is emitted whenever the app-id of the toplevel changes. 90 | 91 | 92 | 93 | 94 | 95 | 96 | This event is emitted whenever the toplevel becomes visible on 97 | the given output. A toplevel may be visible on multiple outputs. 98 | 99 | 100 | 101 | 102 | 103 | 104 | This event is emitted whenever the toplevel stops being visible on 105 | the given output. It is guaranteed that an entered-output event 106 | with the same output has been emitted before this event. 107 | 108 | 109 | 110 | 111 | 112 | 113 | Requests that the toplevel be maximized. If the maximized state actually 114 | changes, this will be indicated by the state event. 115 | 116 | 117 | 118 | 119 | 120 | Requests that the toplevel be unmaximized. If the maximized state actually 121 | changes, this will be indicated by the state event. 122 | 123 | 124 | 125 | 126 | 127 | Requests that the toplevel be minimized. If the minimized state actually 128 | changes, this will be indicated by the state event. 129 | 130 | 131 | 132 | 133 | 134 | Requests that the toplevel be unminimized. If the minimized state actually 135 | changes, this will be indicated by the state event. 136 | 137 | 138 | 139 | 140 | 141 | Request that this toplevel be activated on the given seat. 142 | There is no guarantee the toplevel will be actually activated. 143 | 144 | 145 | 146 | 147 | 148 | 149 | The different states that a toplevel can have. These have the same meaning 150 | as the states with the same names defined in xdg-toplevel 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 162 | is created and each time the toplevel state changes, either because of a 163 | compositor action or because of a request in this protocol. 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | This event is sent after all changes in the toplevel state have been 172 | sent. 173 | 174 | This allows changes to the zwlr_foreign_toplevel_handle_v1 properties 175 | to be seen as atomic, even if they happen via multiple events. 176 | 177 | 178 | 179 | 180 | 181 | Send a request to the toplevel to close itself. The compositor would 182 | typically use a shell-specific method to carry out this request, for 183 | example by sending the xdg_toplevel.close event. However, this gives 184 | no guarantees the toplevel will actually be destroyed. If and when 185 | this happens, the zwlr_foreign_toplevel_handle_v1.closed event will 186 | be emitted. 187 | 188 | 189 | 190 | 191 | 192 | The rectangle of the surface specified in this request corresponds to 193 | the place where the app using this protocol represents the given toplevel. 194 | It can be used by the compositor as a hint for some operations, e.g 195 | minimizing. The client is however not required to set this, in which 196 | case the compositor is free to decide some default value. 197 | 198 | If the client specifies more than one rectangle, only the last one is 199 | considered. 200 | 201 | The dimensions are given in surface-local coordinates. 202 | Setting width=height=0 removes the already-set rectangle. 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 215 | 216 | 217 | 218 | 219 | This event means the toplevel has been destroyed. It is guaranteed there 220 | won't be any more events for this zwlr_foreign_toplevel_handle_v1. The 221 | toplevel itself becomes inert so any requests will be ignored except the 222 | destroy request. 223 | 224 | 225 | 226 | 227 | 228 | Destroys the zwlr_foreign_toplevel_handle_v1 object. 229 | 230 | This request should be called either when the client does not want to 231 | use the toplevel anymore or after the closed event to finalize the 232 | destruction of the object. 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | Requests that the toplevel be fullscreened on the given output. If the 241 | fullscreen state and/or the outputs the toplevel is visible on actually 242 | change, this will be indicated by the state and output_enter/leave 243 | events. 244 | 245 | The output parameter is only a hint to the compositor. Also, if output 246 | is NULL, the compositor should decide which output the toplevel will be 247 | fullscreened on, if at all. 248 | 249 | 250 | 251 | 252 | 253 | 254 | Requests that the toplevel be unfullscreened. If the fullscreen state 255 | actually changes, this will be indicated by the state event. 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | This event is emitted whenever the parent of the toplevel changes. 264 | 265 | No event is emitted when the parent handle is destroyed by the client. 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /protocol/wlr-screencopy-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Simon Ser 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice (including the next 14 | paragraph) shall be included in all copies or substantial portions of the 15 | Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | 27 | This protocol allows clients to ask the compositor to copy part of the 28 | screen content to a client buffer. 29 | 30 | Warning! The protocol described in this file is experimental and 31 | backward incompatible changes may be made. Backward compatible changes 32 | may be added together with the corresponding interface version bump. 33 | Backward incompatible changes are done by bumping the version number in 34 | the protocol and interface names and resetting the interface version. 35 | Once the protocol is to be declared stable, the 'z' prefix and the 36 | version number in the protocol and interface names are removed and the 37 | interface version number is reset. 38 | 39 | 40 | 41 | 42 | This object is a manager which offers requests to start capturing from a 43 | source. 44 | 45 | 46 | 47 | 48 | Capture the next frame of an entire output. 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | Capture the next frame of an output's region. 59 | 60 | The region is given in output logical coordinates, see 61 | xdg_output.logical_size. The region will be clipped to the output's 62 | extents. 63 | 64 | 65 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | All objects created by the manager will still remain valid, until their 77 | appropriate destroy request has been called. 78 | 79 | 80 | 81 | 82 | 83 | 84 | This object represents a single frame. 85 | 86 | When created, a "buffer" event will be sent. The client will then be able 87 | to send a "copy" request. If the capture is successful, the compositor 88 | will send a "flags" followed by a "ready" event. 89 | 90 | If the capture failed, the "failed" event is sent. This can happen anytime 91 | before the "ready" event. 92 | 93 | Once either a "ready" or a "failed" event is received, the client should 94 | destroy the frame. 95 | 96 | 97 | 98 | 99 | Provides information about the frame's buffer. This event is sent once 100 | as soon as the frame is created. 101 | 102 | The client should then create a buffer with the provided attributes, and 103 | send a "copy" request. 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Copy the frame to the supplied buffer. The buffer must have a the 114 | correct size, see zwlr_screencopy_frame_v1.buffer. The buffer needs to 115 | have a supported format. 116 | 117 | If the frame is successfully copied, a "flags" and a "ready" events are 118 | sent. Otherwise, a "failed" event is sent. 119 | 120 | 121 | 122 | 123 | 124 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | Provides flags about the frame. This event is sent once before the 137 | "ready" event. 138 | 139 | 140 | 141 | 142 | 143 | 144 | Called as soon as the frame is copied, indicating it is available 145 | for reading. This event includes the time at which presentation happened 146 | at. 147 | 148 | The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, 149 | each component being an unsigned 32-bit value. Whole seconds are in 150 | tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, 151 | and the additional fractional part in tv_nsec as nanoseconds. Hence, 152 | for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part 153 | may have an arbitrary offset at start. 154 | 155 | After receiving this event, the client should destroy the object. 156 | 157 | 159 | 161 | 163 | 164 | 165 | 166 | 167 | This event indicates that the attempted frame copy has failed. 168 | 169 | After receiving this event, the client should destroy the object. 170 | 171 | 172 | 173 | 174 | 175 | Destroys the frame. This request can be sent at any time by the client. 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /render.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "buffer.h" 9 | #include "output-layout.h" 10 | #include "render.h" 11 | 12 | #include "wlr-screencopy-unstable-v1-protocol.h" 13 | #include "hyprland-toplevel-export-v1-protocol.h" 14 | 15 | static pixman_format_code_t get_pixman_format(enum wl_shm_format wl_fmt) { 16 | switch (wl_fmt) { 17 | #if GRIM_LITTLE_ENDIAN 18 | case WL_SHM_FORMAT_RGB332: 19 | return PIXMAN_r3g3b2; 20 | case WL_SHM_FORMAT_BGR233: 21 | return PIXMAN_b2g3r3; 22 | case WL_SHM_FORMAT_ARGB4444: 23 | return PIXMAN_a4r4g4b4; 24 | case WL_SHM_FORMAT_XRGB4444: 25 | return PIXMAN_x4r4g4b4; 26 | case WL_SHM_FORMAT_ABGR4444: 27 | return PIXMAN_a4b4g4r4; 28 | case WL_SHM_FORMAT_XBGR4444: 29 | return PIXMAN_x4b4g4r4; 30 | case WL_SHM_FORMAT_ARGB1555: 31 | return PIXMAN_a1r5g5b5; 32 | case WL_SHM_FORMAT_XRGB1555: 33 | return PIXMAN_x1r5g5b5; 34 | case WL_SHM_FORMAT_ABGR1555: 35 | return PIXMAN_a1b5g5r5; 36 | case WL_SHM_FORMAT_XBGR1555: 37 | return PIXMAN_x1b5g5r5; 38 | case WL_SHM_FORMAT_RGB565: 39 | return PIXMAN_r5g6b5; 40 | case WL_SHM_FORMAT_BGR565: 41 | return PIXMAN_b5g6r5; 42 | case WL_SHM_FORMAT_RGB888: 43 | return PIXMAN_r8g8b8; 44 | case WL_SHM_FORMAT_BGR888: 45 | return PIXMAN_b8g8r8; 46 | case WL_SHM_FORMAT_ARGB8888: 47 | return PIXMAN_a8r8g8b8; 48 | case WL_SHM_FORMAT_XRGB8888: 49 | return PIXMAN_x8r8g8b8; 50 | case WL_SHM_FORMAT_ABGR8888: 51 | return PIXMAN_a8b8g8r8; 52 | case WL_SHM_FORMAT_XBGR8888: 53 | return PIXMAN_x8b8g8r8; 54 | case WL_SHM_FORMAT_BGRA8888: 55 | return PIXMAN_b8g8r8a8; 56 | case WL_SHM_FORMAT_BGRX8888: 57 | return PIXMAN_b8g8r8x8; 58 | case WL_SHM_FORMAT_RGBA8888: 59 | return PIXMAN_r8g8b8a8; 60 | case WL_SHM_FORMAT_RGBX8888: 61 | return PIXMAN_r8g8b8x8; 62 | case WL_SHM_FORMAT_ARGB2101010: 63 | return PIXMAN_a2r10g10b10; 64 | case WL_SHM_FORMAT_ABGR2101010: 65 | return PIXMAN_a2b10g10r10; 66 | case WL_SHM_FORMAT_XRGB2101010: 67 | return PIXMAN_x2r10g10b10; 68 | case WL_SHM_FORMAT_XBGR2101010: 69 | return PIXMAN_x2b10g10r10; 70 | #else 71 | case WL_SHM_FORMAT_ARGB8888: 72 | return PIXMAN_b8g8r8a8; 73 | case WL_SHM_FORMAT_XRGB8888: 74 | return PIXMAN_b8g8r8x8; 75 | case WL_SHM_FORMAT_ABGR8888: 76 | return PIXMAN_r8g8b8a8; 77 | case WL_SHM_FORMAT_XBGR8888: 78 | return PIXMAN_r8g8b8x8; 79 | case WL_SHM_FORMAT_BGRA8888: 80 | return PIXMAN_a8r8g8b8; 81 | case WL_SHM_FORMAT_BGRX8888: 82 | return PIXMAN_x8r8g8b8; 83 | case WL_SHM_FORMAT_RGBA8888: 84 | return PIXMAN_a8b8g8r8; 85 | case WL_SHM_FORMAT_RGBX8888: 86 | return PIXMAN_x8b8g8r8; 87 | #endif 88 | default: 89 | return 0; 90 | } 91 | } 92 | 93 | static void compute_composite_region(const struct pixman_f_transform *out2com, 94 | int output_width, int output_height, struct grim_box *dest, 95 | bool *grid_aligned) { 96 | struct pixman_transform o2c_fixedpt; 97 | pixman_transform_from_pixman_f_transform(&o2c_fixedpt, out2com); 98 | 99 | pixman_fixed_t w = pixman_int_to_fixed(output_width); 100 | pixman_fixed_t h = pixman_int_to_fixed(output_height); 101 | struct pixman_vector corners[4] = { 102 | {{0, 0, pixman_fixed_1}}, 103 | {{w, 0, pixman_fixed_1}}, 104 | {{0, h, pixman_fixed_1}}, 105 | {{w, h, pixman_fixed_1}}, 106 | }; 107 | 108 | pixman_fixed_t x_min = INT32_MAX, x_max = INT32_MIN, 109 | y_min = INT32_MAX, y_max = INT32_MIN; 110 | for (int i = 0; i < 4; i++) { 111 | pixman_transform_point(&o2c_fixedpt, &corners[i]); 112 | x_min = corners[i].vector[0] < x_min ? corners[i].vector[0] : x_min; 113 | x_max = corners[i].vector[0] > x_max ? corners[i].vector[0] : x_max; 114 | y_min = corners[i].vector[1] < y_min ? corners[i].vector[1] : y_min; 115 | y_max = corners[i].vector[1] > y_max ? corners[i].vector[1] : y_max; 116 | } 117 | 118 | *grid_aligned = pixman_fixed_frac(x_min) == 0 && 119 | pixman_fixed_frac(x_max) == 0 && 120 | pixman_fixed_frac(y_min) == 0 && 121 | pixman_fixed_frac(y_max) == 0; 122 | 123 | int32_t x1 = pixman_fixed_to_int(pixman_fixed_floor(x_min)); 124 | int32_t x2 = pixman_fixed_to_int(pixman_fixed_ceil(x_max)); 125 | int32_t y1 = pixman_fixed_to_int(pixman_fixed_floor(y_min)); 126 | int32_t y2 = pixman_fixed_to_int(pixman_fixed_ceil(y_max)); 127 | *dest = (struct grim_box) { 128 | .x = x1, 129 | .y = y1, 130 | .width = x2 - x1, 131 | .height = y2 - y1 132 | }; 133 | } 134 | 135 | pixman_image_t *render(struct grim_state *state, struct grim_box *geometry, 136 | double scale) { 137 | int common_width = geometry->width * scale; 138 | int common_height = geometry->height * scale; 139 | pixman_image_t *common_image = pixman_image_create_bits(PIXMAN_a8r8g8b8, 140 | common_width, common_height, NULL, 0); 141 | if (!common_image) { 142 | fprintf(stderr, "failed to create image with size: %d x %d\n", 143 | common_width, common_height); 144 | return NULL; 145 | } 146 | 147 | struct grim_output *output; 148 | wl_list_for_each(output, &state->outputs, link) { 149 | struct grim_buffer *buffer = output->buffer; 150 | if (buffer == NULL) { 151 | continue; 152 | } 153 | 154 | pixman_format_code_t pixman_fmt = get_pixman_format(buffer->format); 155 | if (!pixman_fmt) { 156 | fprintf(stderr, "unsupported format %d = 0x%08x\n", 157 | buffer->format, buffer->format); 158 | return NULL; 159 | } 160 | 161 | int32_t output_x = output->logical_geometry.x - geometry->x; 162 | int32_t output_y = output->logical_geometry.y - geometry->y; 163 | int32_t output_width = output->logical_geometry.width; 164 | int32_t output_height = output->logical_geometry.height; 165 | 166 | int32_t raw_output_width = output->geometry.width; 167 | int32_t raw_output_height = output->geometry.height; 168 | apply_output_transform(output->transform, 169 | &raw_output_width, &raw_output_height); 170 | 171 | int output_flipped_x = get_output_flipped(output->transform); 172 | int output_flipped_y = output->screencopy_frame_flags & 173 | (state->use_win 174 | ? HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_Y_INVERT 175 | : ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT) 176 | ? -1 : 1; 177 | 178 | pixman_image_t *output_image = pixman_image_create_bits( 179 | pixman_fmt, buffer->width, buffer->height, 180 | buffer->data, buffer->stride); 181 | if (!output_image) { 182 | fprintf(stderr, "Failed to create image\n"); 183 | return NULL; 184 | } 185 | 186 | // The transformation `out2com` will send a pixel in the output_image 187 | // to one in the common_image 188 | struct pixman_f_transform out2com; 189 | pixman_f_transform_init_identity(&out2com); 190 | pixman_f_transform_translate(&out2com, NULL, 191 | -(double)output->geometry.width / 2, 192 | -(double)output->geometry.height / 2); 193 | pixman_f_transform_scale(&out2com, NULL, 194 | (double)output_width / raw_output_width, 195 | (double)output_height * output_flipped_y / raw_output_height); 196 | pixman_f_transform_rotate(&out2com, NULL, 197 | round(cos(get_output_rotation(output->transform))), 198 | round(sin(get_output_rotation(output->transform)))); 199 | pixman_f_transform_scale(&out2com, NULL, output_flipped_x, 1); 200 | pixman_f_transform_translate(&out2com, NULL, 201 | (double)output_width / 2, 202 | (double)output_height / 2); 203 | pixman_f_transform_translate(&out2com, NULL, output_x, output_y); 204 | pixman_f_transform_scale(&out2com, NULL, scale, scale); 205 | 206 | struct grim_box composite_dest; 207 | bool grid_aligned; 208 | compute_composite_region(&out2com, buffer->width, 209 | buffer->height, &composite_dest, &grid_aligned); 210 | 211 | pixman_f_transform_translate(&out2com, NULL, 212 | -composite_dest.x, -composite_dest.y); 213 | 214 | struct pixman_f_transform com2out; 215 | pixman_f_transform_invert(&com2out, &out2com); 216 | struct pixman_transform c2o_fixedpt; 217 | pixman_transform_from_pixman_f_transform(&c2o_fixedpt, &com2out); 218 | pixman_image_set_transform(output_image, &c2o_fixedpt); 219 | 220 | double x_scale = fmax(fabs(out2com.m[0][0]), fabs(out2com.m[0][1])); 221 | double y_scale = fmax(fabs(out2com.m[1][0]), fabs(out2com.m[1][1])); 222 | if (x_scale >= 0.75 && y_scale >= 0.75) { 223 | // Bilinear scaling is relatively fast and gives decent 224 | // results for upscaling and light downscaling 225 | pixman_image_set_filter(output_image, 226 | PIXMAN_FILTER_BILINEAR, NULL, 0); 227 | } else { 228 | // When downscaling, convolve the output_image so that each 229 | // pixel in the common_image collects colors from a region 230 | // of size roughly 1/x_scale*1/y_scale in the output_image 231 | int n_values = 0; 232 | pixman_fixed_t *conv = pixman_filter_create_separable_convolution( 233 | &n_values, 234 | pixman_double_to_fixed(fmax(1., 1. / x_scale)), 235 | pixman_double_to_fixed(fmax(1., 1. / y_scale)), 236 | PIXMAN_KERNEL_IMPULSE, PIXMAN_KERNEL_IMPULSE, 237 | PIXMAN_KERNEL_LANCZOS2, PIXMAN_KERNEL_LANCZOS2, 238 | 2, 2); 239 | pixman_image_set_filter(output_image, 240 | PIXMAN_FILTER_SEPARABLE_CONVOLUTION, conv, n_values); 241 | free(conv); 242 | } 243 | 244 | bool overlapping = false; 245 | struct grim_output *other_output; 246 | wl_list_for_each(other_output, &state->outputs, link) { 247 | if (output != other_output && intersect_box(&output->logical_geometry, 248 | &other_output->logical_geometry)) { 249 | overlapping = true; 250 | } 251 | } 252 | /* OP_SRC copies the image instead of blending it, and is much 253 | * faster, but this a) is incorrect in the weird case where 254 | * logical outputs overlap and are partially transparent b) 255 | * can draw the edge between two outputs incorrectly if that 256 | * edge is not exactly grid aligned in the common image */ 257 | pixman_op_t op = (grid_aligned && !overlapping) ? PIXMAN_OP_SRC : PIXMAN_OP_OVER; 258 | pixman_image_composite32(op, output_image, NULL, common_image, 259 | 0, 0, 0, 0, composite_dest.x, composite_dest.y, 260 | composite_dest.width, composite_dest.height); 261 | 262 | pixman_image_unref(output_image); 263 | } 264 | 265 | return common_image; 266 | } 267 | -------------------------------------------------------------------------------- /write_jpg.c: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Bernhard R. Fischer, 4096R/8E24F29D bf@abenteuerland.at 3 | * @license This code is free software. Do whatever you like to do with it. 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "write_jpg.h" 17 | 18 | int write_to_jpeg_stream(pixman_image_t *image, FILE *stream, int quality) { 19 | pixman_format_code_t format = pixman_image_get_format(image); 20 | assert(format == PIXMAN_a8r8g8b8 || format == PIXMAN_x8r8g8b8); 21 | 22 | struct jpeg_compress_struct cinfo; 23 | struct jpeg_error_mgr jerr; 24 | JSAMPROW row_pointer[1]; 25 | cinfo.err = jpeg_std_error(&jerr); 26 | jpeg_create_compress(&cinfo); 27 | 28 | unsigned char *data = NULL; 29 | unsigned long len = 0; 30 | jpeg_mem_dest(&cinfo, &data, &len); 31 | cinfo.image_width = pixman_image_get_width(image); 32 | cinfo.image_height = pixman_image_get_height(image); 33 | if (format == PIXMAN_a8r8g8b8) { 34 | cinfo.in_color_space = JCS_EXT_BGRA; 35 | } else { 36 | cinfo.in_color_space = JCS_EXT_BGRX; 37 | } 38 | cinfo.input_components = 4; 39 | 40 | jpeg_set_defaults(&cinfo); 41 | jpeg_set_quality(&cinfo, quality, TRUE); 42 | 43 | jpeg_start_compress(&cinfo, TRUE); 44 | 45 | while (cinfo.next_scanline < cinfo.image_height) { 46 | row_pointer[0] = (unsigned char *)pixman_image_get_data(image) 47 | + (cinfo.next_scanline * pixman_image_get_stride(image)); 48 | (void) jpeg_write_scanlines(&cinfo, row_pointer, 1); 49 | } 50 | 51 | jpeg_finish_compress(&cinfo); 52 | jpeg_destroy_compress(&cinfo); 53 | 54 | size_t written = fwrite(data, 1, len, stream); 55 | if (written < len) { 56 | free(data); 57 | fprintf(stderr, "Failed to write jpg; only %zu of %lu bytes written\n", 58 | written, len); 59 | return -1; 60 | } 61 | free(data); 62 | return 0; 63 | } 64 | -------------------------------------------------------------------------------- /write_png.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "write_png.h" 8 | 9 | static void pack_row32(uint8_t *restrict row_out, const uint32_t *restrict row_in, 10 | size_t width, bool fully_opaque) { 11 | for (size_t x = 0; x < width; x++) { 12 | uint8_t b = (row_in[x] >> 0) & 0xff; 13 | uint8_t g = (row_in[x] >> 8) & 0xff; 14 | uint8_t r = (row_in[x] >> 16) & 0xff; 15 | uint8_t a = (row_in[x] >> 24) & 0xff; 16 | 17 | // Unpremultiply pixels, if necessary. In practice, few images 18 | // made by grim will have many pixels with fractional alpha 19 | if (!fully_opaque && (a != 0 && a != 255)) { 20 | uint32_t inv = (0xff << 16) / a; 21 | uint32_t sr = r * inv; 22 | r = sr > (0xff << 16) ? 0xff : (sr >> 16); 23 | uint32_t sg = g * inv; 24 | g = sg > (0xff << 16) ? 0xff : (sg >> 16); 25 | uint32_t sb = b * inv; 26 | b = sb > (0xff << 16) ? 0xff : (sb >> 16); 27 | } 28 | 29 | *row_out++ = r; 30 | *row_out++ = g; 31 | *row_out++ = b; 32 | if (!fully_opaque) { 33 | *row_out++ = a; 34 | } 35 | } 36 | } 37 | 38 | int write_to_png_stream(pixman_image_t *image, FILE *stream, 39 | int comp_level) { 40 | pixman_format_code_t format = pixman_image_get_format(image); 41 | assert(format == PIXMAN_a8r8g8b8 || format == PIXMAN_x8r8g8b8); 42 | 43 | int width = pixman_image_get_width(image); 44 | int height = pixman_image_get_height(image); 45 | int stride = pixman_image_get_stride(image); 46 | const unsigned char *data = (unsigned char *)pixman_image_get_data(image); 47 | 48 | bool fully_opaque = true; 49 | if (format == PIXMAN_a8r8g8b8) { 50 | for (int y = 0; y < height; y++) { 51 | const uint32_t *row = (const uint32_t *)(data + y * stride); 52 | for (int x = 0; x < width; x++) { 53 | if ((row[x] >> 24) != 0xff) { 54 | fully_opaque = false; 55 | } 56 | } 57 | } 58 | } 59 | int color_type = fully_opaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGBA; 60 | int bit_depth = 8; 61 | 62 | uint8_t *tmp_row = calloc(width, 4); 63 | if (!tmp_row) { 64 | fprintf(stderr, "failed to allocate temp row\n"); 65 | return -1; 66 | } 67 | 68 | int ret = 0; 69 | png_struct *png = png_create_write_struct(PNG_LIBPNG_VER_STRING, 70 | NULL, NULL, NULL); 71 | png_info *info = NULL; 72 | if (!png) { 73 | fprintf(stderr, "failed to allocate png struct\n"); 74 | ret = -1; 75 | goto cleanup; 76 | } 77 | info = png_create_info_struct(png); 78 | if (!info) { 79 | fprintf(stderr, "failed to allocate png write struct\n"); 80 | ret = -1; 81 | goto cleanup; 82 | } 83 | 84 | #ifdef PNG_SETJMP_SUPPORTED 85 | if (setjmp(png_jmpbuf(png))) { 86 | fprintf(stderr, "failed to write png\n"); 87 | ret = -1; 88 | goto cleanup; 89 | } 90 | #endif 91 | 92 | png_init_io(png, stream); 93 | 94 | png_set_IHDR(png, info, width, height, bit_depth, color_type, 95 | PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); 96 | png_write_info(png, info); 97 | 98 | // If the level is zero (no compression), filtering will be unnecessary 99 | png_set_compression_level(png, comp_level); 100 | if (comp_level == 0) { 101 | png_set_filter(png, 0, PNG_NO_FILTERS); 102 | } else { 103 | png_set_filter(png, 0, PNG_ALL_FILTERS); 104 | } 105 | 106 | for (int y = 0; y < height; y++) { 107 | const uint32_t *row = (const uint32_t *)(data + y * stride); 108 | pack_row32(tmp_row, row, width, fully_opaque); 109 | png_write_row(png, tmp_row); 110 | } 111 | 112 | png_write_end(png, NULL); 113 | 114 | cleanup: 115 | if (info) { 116 | png_destroy_info_struct(png, &info); 117 | } 118 | if (png) { 119 | png_destroy_write_struct(&png, NULL); 120 | } 121 | free(tmp_row); 122 | return ret; 123 | } 124 | -------------------------------------------------------------------------------- /write_ppm.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "write_ppm.h" 13 | 14 | int write_to_ppm_stream(pixman_image_t *image, FILE *stream) { 15 | // 256 bytes ought to be enough for everyone 16 | char header[256]; 17 | 18 | int width = pixman_image_get_width(image); 19 | int height = pixman_image_get_height(image); 20 | 21 | int header_len = snprintf(header, sizeof(header), "P6\n%d %d\n255\n", width, height); 22 | assert(header_len <= (int)sizeof(header)); 23 | 24 | size_t len = header_len + width * height * 3; 25 | unsigned char *data = malloc(len); 26 | unsigned char *buffer = data; 27 | 28 | // We _do_not_ include the null byte 29 | memcpy(buffer, header, header_len); 30 | buffer += header_len; 31 | 32 | pixman_format_code_t format = pixman_image_get_format(image); 33 | assert(format == PIXMAN_a8r8g8b8 || format == PIXMAN_x8r8g8b8); 34 | 35 | // Both formats are native-endian 32-bit ints 36 | uint32_t *pixels = pixman_image_get_data(image); 37 | for (int y = 0; y < height; y++) { 38 | for (int x = 0; x < width; x++) { 39 | uint32_t p = *pixels++; 40 | // RGB order 41 | *buffer++ = (p >> 16) & 0xff; 42 | *buffer++ = (p >> 8) & 0xff; 43 | *buffer++ = (p >> 0) & 0xff; 44 | } 45 | } 46 | 47 | size_t written = fwrite(data, 1, len, stream); 48 | if (written < len) { 49 | free(data); 50 | fprintf(stderr, "Failed to write ppm; only %zu of %zu bytes written\n", 51 | written, len); 52 | return -1; 53 | } 54 | free(data); 55 | return 0; 56 | } 57 | --------------------------------------------------------------------------------