Compare commits

...

5 Commits

Author SHA1 Message Date
Nikita ca600da9de
Merge 5774f65767 into 7b2badbb40 2023-09-15 16:12:11 +03:00
Nikita Ermakov 5774f65767 Add info about GIF and JPG to the man page 2023-09-15 16:12:07 +03:00
Nikita Ermakov 9903f02070 Fix CodeQL error
Multiplication result converted to larger type
2023-09-15 16:12:07 +03:00
Nikita Ermakov 832e32f2ef Add libgif-dev to Dockerfile and check for gif_lib.h in configure 2023-09-15 16:12:07 +03:00
Nikita Ermakov 5ab20d6b48 Add GIF support for animated lock screen
Use gif_lib to read a GIF file [1] and show animated lock screen.
Currently it supports only 0-2 disposal methods.

[1] https://www.w3.org/Graphics/GIF/spec-gif89a.txt
2023-09-15 16:12:07 +03:00
8 changed files with 262 additions and 58 deletions

View File

@ -9,7 +9,7 @@ jobs:
- name: Install deps
run: |
sudo apt update
sudo apt install pkg-config libpam0g-dev libcairo2-dev libfontconfig1-dev libxcb-composite0-dev libev-dev libx11-xcb-dev libxcb-xkb-dev libxcb-xinerama0-dev libxcb-randr0-dev libxcb-image0-dev libxcb-util-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev libjpeg-dev
sudo apt install pkg-config libpam0g-dev libcairo2-dev libfontconfig1-dev libxcb-composite0-dev libev-dev libx11-xcb-dev libxcb-xkb-dev libxcb-xinerama0-dev libxcb-randr0-dev libxcb-image0-dev libxcb-util-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev libjpeg-dev libgif-dev
- name: Build
run: ./build.sh
- name: Check and distcheck

View File

@ -66,7 +66,7 @@ jobs:
- run: |
sudo apt-get update
sudo apt install autoconf gcc make pkg-config libpam0g-dev libcairo2-dev libfontconfig1-dev libxcb-composite0-dev libev-dev libx11-xcb-dev libxcb-xkb-dev libxcb-xinerama0-dev libxcb-randr0-dev libxcb-image0-dev libxcb-util-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev libjpeg-dev
sudo apt install autoconf gcc make pkg-config libpam0g-dev libcairo2-dev libfontconfig1-dev libxcb-composite0-dev libev-dev libx11-xcb-dev libxcb-xkb-dev libxcb-xinerama0-dev libxcb-randr0-dev libxcb-image0-dev libxcb-util-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev libjpeg-dev libgif-dev
./build.sh
- name: Perform CodeQL Analysis

View File

@ -76,6 +76,7 @@ AC_SEARCH_LIBS([ev_run], [ev], , [AC_MSG_FAILURE([cannot find the required ev_ru
AC_SEARCH_LIBS([shm_open], [rt])
AC_SEARCH_LIBS([DGifOpenFileName], [gif])
# Use system-local-login instead of login on Arch and Gentoo
if [[ -f /etc/arch-release ]] || [[ -f /etc/gentoo-release ]]; then
echo "Using PAM for Arch/Gentoo"
@ -128,7 +129,7 @@ AX_CHECK_BASH_COMPLETION
AX_CHECK_ZSH_COMPLETION
# Checks for header files.
AC_CHECK_HEADERS([fcntl.h float.h inttypes.h limits.h locale.h netinet/in.h paths.h stddef.h stdint.h stdlib.h string.h sys/param.h sys/socket.h sys/time.h unistd.h], , [AC_MSG_FAILURE([cannot find the $ac_header header, which i3lock requires])])
AC_CHECK_HEADERS([fcntl.h float.h inttypes.h limits.h locale.h netinet/in.h paths.h stddef.h stdint.h stdlib.h string.h sys/param.h sys/socket.h sys/time.h unistd.h gif_lib.h], , [AC_MSG_FAILURE([cannot find the $ac_header header, which i3lock requires])])
AC_CONFIG_FILES([Makefile])

View File

@ -8,7 +8,7 @@
.fi
..
.TH i3lock-color 1 "JAN 2022" Linux "User Manuals"
.TH i3lock-color 1 "SEP 2022" Linux "User Manuals"
.SH NAME
i3lock-color \- improved screen locker
@ -42,8 +42,8 @@ i3lock forks, so you can combine it with an alias to suspend to RAM
(run "i3lock && echo mem > /sys/power/state" to get a locked screen after waking
up your computer from suspend to RAM)
.IP \[bu]
You can specify either a background color or a PNG image which will be displayed
while your screen is locked.
You can specify either a PNG or JPG image, a GIF animation or a background
color which will be displayed while your screen is locked.
.IP \[bu]
You can specify whether i3lock should bell upon a wrong password.
.IP \[bu]
@ -73,12 +73,12 @@ verified or whether it is wrong).
.TP
.BI \-i\ path \fR,\ \fB\-\-image= path
Display the given PNG image instead of a blank screen.
Display the given PNG/JPG image or GIF animation instead of a blank screen.
.TP
.BI \fB\-\-raw= format
Read the image given by \-\-image as a raw image instead of PNG. The argument is
the image's format as <width>x<height>:<pixfmt>.
Read the image given by \-\-image as a raw image instead of PNG/JPG/GIF. The
argument is the image's format as <width>x<height>:<pixfmt>.
The supported pixel formats are:
\'native', 'rgb', 'xrgb', 'rgbx', 'bgr', 'xbgr', and 'bgrx'.
The "native" pixel format expects a pixel as a 32-bit (4-byte) integer in

281
i3lock.c
View File

@ -61,6 +61,8 @@
#include "jpg.h"
#include "fonts.h"
#include <gif_lib.h>
#define TSTAMP_N_SECS(n) (n * 1.0)
#define TSTAMP_N_MINS(n) (60 * TSTAMP_N_SECS(n))
#define START_TIMER(timer_obj, timeout, callback) \
@ -247,6 +249,14 @@ char *image_path = NULL;
char *image_raw_format = NULL;
char *slideshow_path = NULL;
struct gif {
cairo_surface_t *img;
float delay_sec;
};
struct gif *gif_img = NULL;
int gif_img_count = 0;
cairo_surface_t *img = NULL;
char *img_slideshow[256];
cairo_surface_t *blur_bg_img = NULL;
@ -313,6 +323,14 @@ char bar_width_expr[32] = ""; // empty string means full width based on bar orie
bool bar_bidirectional = false;
bool bar_reversed = false;
enum IMAGE_FORMAT {
IMAGE_FORMAT_UNKNOWN,
IMAGE_FORMAT_RAW,
IMAGE_FORMAT_PNG,
IMAGE_FORMAT_JPG,
IMAGE_FORMAT_GIF
};
/* isutf, u8_dec © 2005 Jeff Bezanson, public domain */
#define isutf(c) (((c)&0xC0) != 0x80)
@ -1140,6 +1158,131 @@ static const struct raw_pixel_format raw_fmt_bgr = {3, 2, 1, 0};
static const struct raw_pixel_format raw_fmt_bgrx = {4, 2, 1, 0};
static const struct raw_pixel_format raw_fmt_xbgr = {4, 3, 2, 1};
static cairo_surface_t *read_gif_image(const char *image_path) {
int err;
int width, stride, height;
int bg_idx;
uint32_t bg_color;
ColorMapObject *cmap;
GraphicsControlBlock gc;
/* Open and load a GIF file */
GifFileType *gif = DGifOpenFileName(image_path, &err);
if (!gif) {
fprintf(stderr, "Could not open GIF file, (Error %d)\n", err);
return NULL;
}
if (DGifSlurp(gif) != GIF_OK) {
fprintf(stderr, "Could not read the GIF image, (Error %d)\n", gif->Error);
goto read_gif_image_clean;
}
/* Load canvas properties */
width = gif->SWidth;
height = gif->SHeight;
cmap = gif->SColorMap;
bg_idx = gif->SBackGroundColor;
GifColorType *cmap_bg_rgb = cmap->Colors + bg_idx;
bg_color = cmap_bg_rgb->Blue | cmap_bg_rgb->Green << 8 | cmap_bg_rgb->Red << 16;
stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, width) / sizeof(uint32_t);
if (stride < 0) {
fprintf(stderr, "Invalid distance between beginning of rows\n");
goto read_gif_image_clean;
}
/* Read the images into the RGB buffers */
gif_img = malloc(gif->ImageCount * sizeof(struct gif));
gif_img_count = gif->ImageCount;
if (!gif_img) {
fprintf(stderr, "Could not allocate memory for GIF image buffers\n");
goto read_gif_image_clean;
}
uint32_t *data_prev = NULL;
for (SavedImage *pimg = gif->SavedImages; pimg < gif->SavedImages + gif->ImageCount; ++pimg) {
int idx = pimg - gif->SavedImages;
/* Create image surface */
gif_img[idx].img = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
if (cairo_surface_status(gif_img[idx].img) != CAIRO_STATUS_SUCCESS) {
fprintf(stderr, "Could not create surface: %s\n",
cairo_status_to_string(cairo_surface_status(gif_img[idx].img)));
free(gif_img);
gif_img = NULL;
goto read_gif_image_clean;
}
cairo_surface_flush(gif_img[idx].img);
uint32_t *data = (uint32_t *)cairo_image_surface_get_data(gif_img[idx].img);
// TODO: Check L,R,W,H && color map?
/* Find Graphics Control extension and delay */
for (int iext = 0; iext < pimg->ExtensionBlockCount; ++iext) {
ExtensionBlock *pext = pimg->ExtensionBlocks + iext;
switch (pext->Function) {
case GRAPHICS_EXT_FUNC_CODE:
DGifExtensionToGCB(pext->ByteCount, pext->Bytes, &gc);
// Delay time is in [ms]. Scale it to seconds.
gif_img[idx].delay_sec = gc.DelayTime*0.01;
break;
default:
gif_img[idx].delay_sec = 0;
}
}
/* Set general image attributes */
ColorMapObject *cmap_img = pimg->ImageDesc.ColorMap;
if (!cmap_img) cmap_img = cmap;
int img_left = pimg->ImageDesc.Left;
int img_width = pimg->ImageDesc.Width;
int img_right = img_left + img_width;
int img_top = pimg->ImageDesc.Top;
int img_bottom = img_top + pimg->ImageDesc.Height;
/* Handle disposal mode */
int data_size = width * height * ((int)sizeof(width));
switch (gc.DisposalMode) {
case DISPOSE_DO_NOT:
if (data_prev) {
memcpy(data, data_prev, data_size);
} else {
memset(data, 0, data_size);
}
break;
case DISPOSE_BACKGROUND:
memset(data, bg_color, data_size);
break;
default:
memset(data, 0, data_size);
}
/* Read RGB */
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
int color_idx;
if (x < img_left || x >= img_right || y < img_top || y >= img_bottom) {
color_idx = bg_idx;
} else {
color_idx = *(pimg->RasterBits + (x - img_left) + ((y - img_top) * img_width));
}
if (color_idx != gc.TransparentColor) {
GifColorType *cmap_rgb = cmap_img->Colors + color_idx;
uint32_t rgb = cmap_rgb->Blue | cmap_rgb->Green << 8 | cmap_rgb->Red << 16;
data[x + (y*width)] = rgb;
}
}
}
data_prev = data;
cairo_surface_mark_dirty(gif_img[idx].img);
}
read_gif_image_clean:
if (!DGifCloseFile(gif, &err)) {
DEBUG("DGifCloseFile call failed, (Error %d)\n", err);
}
return gif_img[0].img;
}
static cairo_surface_t *read_raw_image(const char *image_path, const char *image_raw_format) {
cairo_surface_t *img;
@ -1171,7 +1314,7 @@ static cairo_surface_t *read_raw_image(const char *image_path, const char *image
uint32_t *data = (uint32_t *)cairo_image_surface_get_data(img);
const int pixstride = cairo_image_surface_get_stride(img) / 4;
FILE *f = fopen(image_path, "r");
FILE *f = fopen(image_path, "rb");
if (f == NULL) {
fprintf(stderr, "Could not open image \"%s\": %s\n",
image_path, strerror(errno));
@ -1233,23 +1376,13 @@ static cairo_surface_t *read_raw_image(const char *image_path, const char *image
return img;
}
static bool verify_png_image(const char *image_path) {
if (!image_path) {
return false;
}
/* Check file exists and has correct PNG header */
FILE *png_file = fopen(image_path, "r");
if (png_file == NULL) {
DEBUG("Image file path \"%s\" cannot be opened: %s\n", image_path, strerror(errno));
return false;
}
static bool verify_png_image(FILE *png_file) {
unsigned char png_header[8];
memset(png_header, '\0', sizeof(png_header));
fseek(png_file, 0, SEEK_SET);
int bytes_read = fread(png_header, 1, sizeof(png_header), png_file);
fclose(png_file);
if (bytes_read != sizeof(png_header)) {
DEBUG("Could not read PNG header from \"%s\"\n", image_path);
DEBUG("Could not read PNG header\n");
return false;
}
@ -1257,12 +1390,67 @@ static bool verify_png_image(const char *image_path) {
// https://www.w3.org/TR/2003/REC-PNG-20031110/#5PNG-file-signature
static unsigned char PNG_REFERENCE_HEADER[8] = {137, 80, 78, 71, 13, 10, 26, 10};
if (memcmp(PNG_REFERENCE_HEADER, png_header, sizeof(png_header)) != 0) {
DEBUG("File \"%s\" does not start with a PNG header. i3lock currently only supports loading PNG files.\n", image_path);
return false;
}
return true;
}
static bool verify_gif_image(FILE *gif_file) {
unsigned char gif_header[6] = {0};
fseek(gif_file, 0, SEEK_SET);
int bytes_read = fread(gif_header, 1, sizeof(gif_header), gif_file);
if (bytes_read != sizeof(gif_header)) {
fprintf(stderr, "Could not read GIF header\n");
return false;
}
// Check GIF header according to the specification, available at:
// https://www.w3.org/Graphics/GIF/spec-gif89a.txt
static const unsigned char GIF_REFERENCE_HEADER[] = {'G', 'I', 'F'};
if (memcmp(GIF_REFERENCE_HEADER, gif_header, sizeof(GIF_REFERENCE_HEADER)) == 0) {
if (gif_header[3] >= '0' && gif_header[3] <= '9' &&
gif_header[4] >= '0' && gif_header[4] <= '9' &&
gif_header[5] >= 'a' && gif_header[5] <= 'z') {
return true;
}
}
return false;
}
static enum IMAGE_FORMAT verify_image(const char *image_path) {
if (!image_path) {
return false;
}
if (image_raw_format != NULL) {
return IMAGE_FORMAT_RAW;
}
/* Check file exists and has correct header */
FILE *file = fopen(image_path, "rb");
if (file == NULL) {
fprintf(stderr, "Image file path \"%s\" cannot be opened (Error %s)\n", image_path, strerror(errno));
return false;
}
enum IMAGE_FORMAT format = IMAGE_FORMAT_UNKNOWN;
if (image_raw_format != NULL) {
format = IMAGE_FORMAT_RAW;
} else if (verify_png_image(file)) {
format = IMAGE_FORMAT_PNG;
} else if (file_is_jpg(file)) {
format = IMAGE_FORMAT_JPG;
} else if (verify_gif_image(file)) {
format = IMAGE_FORMAT_GIF;
}
fclose(file);
return format;
}
#ifndef __OpenBSD__
/*
* Callback function for PAM. We only react on password request callbacks.
@ -1454,31 +1642,39 @@ static void raise_loop(xcb_window_t window) {
/*
* Loads an image from the given path. Handles JPEG and PNG. Returns NULL in case of error.
*/
cairo_surface_t* load_image(char* image_path) {
cairo_surface_t *load_image(enum IMAGE_FORMAT format) {
cairo_surface_t *img = NULL;
JPEG_INFO jpg_info;
unsigned char *jpg_data;
if (image_raw_format != NULL && image_path != NULL) {
/* Read image. 'read_raw_image' returns NULL on error,
* so we don't have to handle errors here. */
img = read_raw_image(image_path, image_raw_format);
} else if (verify_png_image(image_path)) {
/* Create a pixmap to render on, fill it with the background color */
img = cairo_image_surface_create_from_png(image_path);
} else if (file_is_jpg(image_path)) {
DEBUG("Image looks like a jpeg, decoding\n");
unsigned char* jpg_data = read_JPEG_file(image_path, &jpg_info);
switch (format) {
case IMAGE_FORMAT_RAW:
/* Read image. 'read_raw_image' returns NULL on error,
* so we don't have to handle errors here. */
img = read_raw_image(image_path, image_raw_format);
break;
case IMAGE_FORMAT_PNG:
img = cairo_image_surface_create_from_png(image_path);
break;
case IMAGE_FORMAT_JPG:
jpg_data = read_JPEG_file(image_path, &jpg_info);
if (jpg_data != NULL) {
img = cairo_image_surface_create_for_data(jpg_data,
CAIRO_FORMAT_ARGB32, jpg_info.width, jpg_info.height,
jpg_info.stride);
CAIRO_FORMAT_ARGB32, jpg_info.width, jpg_info.height,
jpg_info.stride);
}
break;
case IMAGE_FORMAT_GIF:
img = read_gif_image(image_path);
break;
default:
fprintf(stderr, "Unsupported image file format: %s\n", image_path);
}
/* In case loading failed, we just pretend no -i was specified. */
if (img && cairo_surface_status(img) != CAIRO_STATUS_SUCCESS) {
fprintf(stderr, "Could not load image \"%s\": %s\n",
image_path, cairo_status_to_string(cairo_surface_status(img)));
fprintf(stderr, "Could not load image, %s\n",
cairo_status_to_string(cairo_surface_status(img)));
img = NULL;
}
@ -1534,6 +1730,17 @@ bool load_slideshow_images(const char *path) {
return true;
}
void gif_anim_loop(struct ev_loop *loop, struct ev_timer *timer, int delay) {
static int img_count = 0;
if (++img_count >= gif_img_count) img_count = 0;
img = gif_img[img_count].img;
ev_timer_stop(loop, timer);
redraw_screen();
ev_timer_set(timer, gif_img[img_count].delay_sec, 0.);
ev_timer_start(loop, timer);
}
int main(int argc, char *argv[]) {
struct passwd *pw;
char *username;
@ -2520,17 +2727,17 @@ int main(int argc, char *argv[]) {
init_colors_once();
if (image_path != NULL) {
if (!is_directory(image_path)) {
img = load_image(image_path);
enum IMAGE_FORMAT image_format = verify_image(image_path);
img = load_image(image_format);
} else {
/* Path to a directory is provided -> use slideshow mode */
slideshow_path = strdup(image_path);
if (!load_slideshow_images(slideshow_path)) exit(EXIT_FAILURE);
img = load_image(img_slideshow[0]);
enum IMAGE_FORMAT image_format = verify_image(img_slideshow[0]);
img = load_image(image_format);
}
free(image_path);
}
free(image_raw_format);
if (blur) {
@ -2611,6 +2818,7 @@ int main(int argc, char *argv[]) {
struct ev_io *xcb_watcher = calloc(sizeof(struct ev_io), 1);
struct ev_check *xcb_check = calloc(sizeof(struct ev_check), 1);
struct ev_prepare *xcb_prepare = calloc(sizeof(struct ev_prepare), 1);
struct ev_timer *xcb_timer = calloc(sizeof(struct ev_timer), 1);
ev_io_init(xcb_watcher, xcb_got_event, xcb_get_file_descriptor(conn), EV_READ);
ev_io_start(main_loop, xcb_watcher);
@ -2621,6 +2829,11 @@ int main(int argc, char *argv[]) {
ev_prepare_init(xcb_prepare, xcb_prepare_cb);
ev_prepare_start(main_loop, xcb_prepare);
if (gif_img) {
ev_timer_init(xcb_timer, gif_anim_loop, gif_img[0].delay_sec, 0.);
ev_timer_start(main_loop, xcb_timer);
}
/* Invoke the event callback once to catch all the events which were
* received up until now. ev will only pick up new events (when the X11
* file descriptor becomes readable). */

16
jpg.c
View File

@ -13,27 +13,17 @@
/*
* Checks if the file is a JPEG by looking for a valid JPEG header.
*/
bool file_is_jpg(char* file_path) {
if (!file_path) return false;
FILE* image_file;
bool file_is_jpg(FILE* image_file) {
uint16_t file_header;
size_t read_count;
// TODO: Consider endianess on non-x86 platforms
uint16_t jpg_magick = 0xd8ff;
image_file = fopen(file_path, "rb");
if (image_file == NULL) {
int img_err = errno;
fprintf(stderr, "Could not open image file %s: %s\n",
file_path, strerror(img_err));
return false;
}
fseek(image_file, 0, SEEK_SET);
read_count = fread(&file_header, sizeof(file_header), 1, image_file);
fclose(image_file);
if (read_count < 1) {
fprintf(stderr, "Error searching for JPEG header in %s\n", file_path);
fprintf(stderr, "Error searching for JPEG header\n");
return false;
}

2
jpg.h
View File

@ -13,7 +13,7 @@ typedef struct {
/*
* Checks if the file is a JPEG by looking for a valid JPEG header.
*/
bool file_is_jpg(char* file_path);
bool file_is_jpg(FILE* image_file);
/*
* Reads a JPEG from a file into memory, in a format that Cairo can create a

View File

@ -19,7 +19,7 @@ RUN apt-get update && \
build-essential clang git autoconf automake libxcb-randr0-dev pkg-config libpam0g-dev \
libcairo2-dev libxcb1-dev libxcb-dpms0-dev libxcb-image0-dev libxcb-util0-dev \
libxcb-xrm-dev libev-dev libxcb-xinerama0-dev libxcb-xkb-dev libxkbcommon-dev \
libxkbcommon-x11-dev clang-format-9 && \
libxkbcommon-x11-dev clang-format-9 libgif-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src