Skip to content

Commit

Permalink
Add GIF support for animated lock screen
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sh1r4s3 committed Jun 26, 2022
1 parent d846e7c commit 85a0f71
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 48 deletions.
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
284 changes: 250 additions & 34 deletions i3lock.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
#include "jpg.h"
#include "fonts.h"

#include <gif_lib.h>
#include <arpa/inet.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) \
Expand Down Expand Up @@ -247,7 +250,16 @@ 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;
bool tile = false;
char *img_slideshow[256];
cairo_surface_t *blur_bg_img = NULL;
int slideshow_image_count = 0;
Expand Down Expand Up @@ -294,6 +306,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)

Expand Down Expand Up @@ -1030,6 +1050,130 @@ 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, err=%d\n", err);
return NULL;
}
if (DGifSlurp(gif) != GIF_OK) {
printf("Could not read the GIF image\n");
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 = 0 | 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 stride\n");
goto read_gif_image_clean;
}

/* Read the images into the RGB buffers */
// TODO: check C char specification, whether to just use int
gif_img = malloc(gif->ImageCount * sizeof(struct gif));
gif_img_count = gif->ImageCount;
if (!gif_img) {
fprintf(stderr, "Can't 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: Chek 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);
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 */
switch (gc.DisposalMode) {
case DISPOSE_DO_NOT:
if (data_prev) {
memcpy(data, data_prev, width*height*sizeof(uint32_t));
} else {
memset(data, 0, width*height*sizeof(uint32_t));
}
break;
case DISPOSE_BACKGROUND:
memset(data, bg_color, width*height*sizeof(uint32_t));
break;
default:
memset(data, 0, width*height*sizeof(uint32_t));
}

/* 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 = 0 | 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)) {
fprintf(stderr, "DGifCloseFile failed with err=%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;

Expand Down Expand Up @@ -1123,36 +1267,81 @@ 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;
}

// Check PNG header according to the specification, available at:
// 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, "r");
if (file == NULL) {
fprintf(stderr, "Image file path \"%s\" cannot be opened: %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.
Expand Down Expand Up @@ -1344,31 +1533,41 @@ 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;

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);
unsigned char *jpg_data;

/* Create a pixmap to render on, fill it with the background color */
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:
/* Create a pixmap to render on, fill it with the background color */
img = cairo_image_surface_create_from_png(image_path);
break;
case IMAGE_FORMAT_GIF:
img = read_gif_image(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;
default:
fprintf(stderr, "Image format of the file %s is unknown\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;
}

Expand Down Expand Up @@ -1424,6 +1623,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;
redraw_screen();
ev_timer_stop(loop, timer);
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;
Expand Down Expand Up @@ -2340,17 +2550,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) {
Expand Down Expand Up @@ -2431,6 +2641,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);
Expand All @@ -2441,6 +2652,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). */
Expand Down
Loading

0 comments on commit 85a0f71

Please sign in to comment.