diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index ab0456b..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -Please do not send/request features related to image manipulation. i3lock’s -support for background images is intentionally kept minimal, you should do all -pre-processing in external tools. diff --git a/Makefile.am b/Makefile.am index 7fd4e6a..ba209d9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -21,7 +21,8 @@ i3lock_CFLAGS = \ $(XCB_UTIL_CFLAGS) \ $(XKBCOMMON_CFLAGS) \ $(CAIRO_CFLAGS) \ - $(CODE_COVERAGE_CFLAGS) + $(CODE_COVERAGE_CFLAGS) \ + $(JPEG_CFLAGS) i3lock_CPPFLAGS = \ $(AM_CPPFLAGS) \ @@ -33,7 +34,8 @@ i3lock_LDADD = \ $(XCB_UTIL_LIBS) \ $(XKBCOMMON_LIBS) \ $(CAIRO_LIBS) \ - $(CODE_COVERAGE_LDFLAGS) + $(CODE_COVERAGE_LDFLAGS) \ + $(JPEG_LIBS) i3lock_SOURCES = \ cursors.h \ @@ -49,7 +51,9 @@ i3lock_SOURCES = \ tinyexpr.h \ blur_simd.c \ blur.c \ - blur.h + blur.h \ + jpg.c \ + jpg.h EXTRA_DIST = \ diff --git a/README.md b/README.md index 9d6064e..6a10f0f 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ i3lock now uses GNU autotools for building; you'll need to do something like `au - libx11-xcb-dev - libxkbcommon >= 0.5.0 - libxkbcommon-x11 >= 0.5.0 +- libjpeg-turbo >= 1.4.90 (On centos/RHEL/etc, the packages tend to look like `cairo-devel` instead of `libcairo-dev`. Use `yum provides \*/` to figure out what packages you need.) @@ -97,6 +98,8 @@ i3lock now uses GNU autotools for building; you'll need to do something like `au sudo apt-get install pkg-config libxcb1 libpam-dev libcairo-dev libxcb-composite0 libxcb-composite0-dev libxcb-xinerama0-dev libev-dev libx11-dev libx11-xcb-dev libxkbcommon0 libxkbcommon-x11-0 libxcb-dpms0-dev libxcb-image0-dev libxcb-util0-dev libxcb-xkb-dev libxkbcommon-x11-dev libxkbcommon-dev +For JPEG lock image support, you'll also need `libjpeg-turbo8` version 1.4.90 or newer (Ubuntu 17.04 or later) + ##### Aur Package [Stable](https://aur.archlinux.org/packages/i3lock-color/) diff --git a/configure.ac b/configure.ac index 1c62878..e122779 100644 --- a/configure.ac +++ b/configure.ac @@ -87,6 +87,8 @@ PKG_CHECK_MODULES([XCB_IMAGE], [xcb-image]) PKG_CHECK_MODULES([XCB_UTIL], [xcb-event xcb-util xcb-atom]) PKG_CHECK_MODULES([XKBCOMMON], [xkbcommon xkbcommon-x11]) PKG_CHECK_MODULES([CAIRO], [cairo]) +PKG_CHECK_MODULES([JPEG], [libjpeg]) + # Checks for programs. AC_PROG_AWK diff --git a/i3lock.c b/i3lock.c index 0e304b7..4ae3852 100644 --- a/i3lock.c +++ b/i3lock.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #ifdef __OpenBSD__ #include @@ -52,6 +53,7 @@ #include "unlock_indicator.h" #include "randr.h" #include "blur.h" +#include "jpg.h" #define TSTAMP_N_SECS(n) (n * 1.0) #define TSTAMP_N_MINS(n) (60 * TSTAMP_N_SECS(n)) @@ -991,6 +993,7 @@ int main(int argc, char *argv[]) { struct passwd *pw; char *username; char *image_path = NULL; + JPEG_INFO jpg_info; #ifndef __OpenBSD__ int ret; struct pam_conv conv = {conv_callback, NULL}; @@ -1705,7 +1708,20 @@ int main(int argc, char *argv[]) { if (image_path) { /* Create a pixmap to render on, fill it with the background color */ - img = cairo_image_surface_create_from_png(image_path); + if (file_is_jpg(image_path)) { + if (debug_mode) { + fprintf(stderr, "Image looks like a jpeg, decoding\n"); + } + + unsigned char* 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); + } + } else { + img = cairo_image_surface_create_from_png(image_path); + } /* In case loading failed, we just pretend no -i was specified. */ if (cairo_surface_status(img) != CAIRO_STATUS_SUCCESS) { fprintf(stderr, "Could not load image \"%s\": %s\n", diff --git a/jpg.c b/jpg.c new file mode 100644 index 0000000..336014c --- /dev/null +++ b/jpg.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jpg.h" + +/* + * Checks if the file is a JPEG by looking for a valid JPEG header. + */ +bool file_is_jpg(char* file_path) { + 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; + } + + 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); + return false; + } + + return file_header == jpg_magick; +} + +/* + * Reads a JPEG from a file into memory, in a format that Cairo can create a + * surface from. + */ +void* read_JPEG_file(char *file_path, JPEG_INFO *jpg_info) { + int img_err; + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + FILE *infile; /* source file */ + void *img; /* decompressed image data pointer */ + + if ((infile = fopen(file_path, "rb")) == NULL) { + img_err = errno; + fprintf(stderr, "Could not open image file %s: %s\n", + file_path, strerror(img_err)); + return NULL; + } + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + + jpeg_stdio_src(&cinfo, infile); + + (void) jpeg_read_header(&cinfo, TRUE); + + // BGRA + endianness change = RGBA? + // TODO: Test this code on non-x86_64 platforms + cinfo.out_color_space = JCS_EXT_BGRA; + + (void) jpeg_start_decompress(&cinfo); + + jpg_info->height = cinfo.output_height; + jpg_info->width = cinfo.output_width; + + /* Get the *cairo* stride rather than the stride from the image. This is + * the space needed in memory for each row for optimized Cairo rendering. */ + int cairo_stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, + cinfo.output_width); + jpg_info->stride = cairo_stride; + if (cairo_stride < jpg_info->width) { + /* This should never happen, but if it does then the following code + * will potentially write into unallocated memory */ + fprintf( + stderr, + "WARNING: Cairo stride shorter than JPEG width. Aborting JPEG read." + ); + return NULL; + } + + // Allocate storage for the final, decompressed image. + img = calloc(cairo_stride, cinfo.output_height); + if (img == NULL) { + fprintf(stderr, "Could not allocate memory for JPEG decode\n"); + + (void) jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(infile); + + return NULL; + } + + while (cinfo.output_scanline < cinfo.output_height) { + /* Normally, you would allocate a buffer using libJPEG's memory + * management and write into it, but since we're reading one row at a + * time, we just write it directly into the image memory space */ + unsigned char* pos = img + (cairo_stride * (cinfo.output_scanline)); + (void) jpeg_read_scanlines(&cinfo, &pos, 1); + } + + (void) jpeg_finish_decompress(&cinfo); + + jpeg_destroy_decompress(&cinfo); + fclose(infile); + + return img; +} diff --git a/jpg.h b/jpg.h new file mode 100644 index 0000000..b1146bc --- /dev/null +++ b/jpg.h @@ -0,0 +1,21 @@ +#ifndef _JPG_H +#define _JPG_H + +typedef struct { + uint height; + uint width; + uint stride; // The width of each row in memory, in bytes +} JPEG_INFO; + +/* + * Checks if the file is a JPEG by looking for a valid JPEG header. + */ +bool file_is_jpg(char* file_path); + +/* + * Reads a JPEG from a file into memory, in a format that Cairo can create a + * surface from. + */ +void* read_JPEG_file(char *filename, JPEG_INFO *jpg_info); + +#endif