diff --git a/.builds/alpine.yml b/.builds/alpine.yml index f438c441..bea21503 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -6,11 +6,16 @@ packages: - wayland-protocols - cairo-dev - pango-dev - - gdk-pixbuf-dev - scdoc sources: - https://github.com/emersion/mako + - https://gitlab.freedesktop.org/emersion/keulim.git tasks: + - keulim: | + cd keulim + meson setup build/ + ninja -C build/ + sudo ninja -C build/ install - setup: | cd mako meson setup build/ --fatal-meson-warnings -Dauto_features=enabled -Dsd-bus-provider=libelogind diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index 4b5410b2..1a790d51 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -5,7 +5,7 @@ packages: - wayland-protocols - cairo - pango - - gdk-pixbuf2 + - keulim-git - scdoc - systemd sources: diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index 1ee5e1f4..f6bdc81a 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -2,7 +2,6 @@ image: freebsd/latest packages: - basu - evdev-proto - - gdk-pixbuf2 - libepoll-shim - meson - pango @@ -12,7 +11,13 @@ packages: - wayland-protocols sources: - https://github.com/emersion/mako + - https://gitlab.freedesktop.org/emersion/keulim.git tasks: + - keulim: | + cd keulim + meson setup build/ + ninja -C build/ + sudo ninja -C build/ install - setup: | cd mako meson setup build/ --fatal-meson-warnings -Dauto_features=enabled -Dsd-bus-provider=basu diff --git a/.gitignore b/.gitignore index d73983bf..02cf3c09 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/subprojects/ +/subprojects/* +!/subprojects/*.wrap diff --git a/README.md b/README.md index ccf9e792..7d5fc2eb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Install dependencies: * pango * cairo * systemd, elogind or [basu] (for the sd-bus library) -* gdk-pixbuf (optional, for icons support) +* [keulim] (optional, for icons support) * dbus (runtime dependency, user-session support is required) * scdoc (optional, for man pages) @@ -69,3 +69,4 @@ MIT [irc]: https://web.libera.chat/gamja/#emersion [spec]: https://specifications.freedesktop.org/notification-spec/latest/ [basu]: https://github.com/emersion/basu +[keulim]: https://gitlab.freedesktop.org/emersion/keulim diff --git a/cairo-pixbuf.c b/cairo-pixbuf.c deleted file mode 100644 index 0d730cfb..00000000 --- a/cairo-pixbuf.c +++ /dev/null @@ -1,91 +0,0 @@ -#include "cairo-pixbuf.h" - -cairo_surface_t *create_cairo_surface_from_gdk_pixbuf(const GdkPixbuf *gdkbuf) { - int chan = gdk_pixbuf_get_n_channels(gdkbuf); - if (chan < 3) { - return NULL; - } - - const guint8* gdkpix = gdk_pixbuf_read_pixels(gdkbuf); - if (!gdkpix) { - return NULL; - } - gint w = gdk_pixbuf_get_width(gdkbuf); - gint h = gdk_pixbuf_get_height(gdkbuf); - int stride = gdk_pixbuf_get_rowstride(gdkbuf); - - cairo_format_t fmt = (chan == 3) ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32; - cairo_surface_t * cs = cairo_image_surface_create(fmt, w, h); - cairo_surface_flush(cs); - if (!cs || cairo_surface_status(cs) != CAIRO_STATUS_SUCCESS) { - return NULL; - } - - int cstride = cairo_image_surface_get_stride(cs); - unsigned char *cpix = cairo_image_surface_get_data(cs); - - if (chan == 3) { - for (int i = h; i; --i) { - const guint8 *gp = gdkpix; - unsigned char *cp = cpix; - const guint8* end = gp + 3*w; - while (gp < end) { -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - cp[0] = gp[2]; - cp[1] = gp[1]; - cp[2] = gp[0]; -#else - cp[1] = gp[0]; - cp[2] = gp[1]; - cp[3] = gp[2]; -#endif - gp += 3; - cp += 4; - } - gdkpix += stride; - cpix += cstride; - } - } else { - /* premul-color = alpha/255 * color/255 * 255 = (alpha*color)/255 - * (z/255) = z/256 * 256/255 = z/256 (1 + 1/255) - * = z/256 + (z/256)/255 = (z + z/255)/256 - * # recurse once - * = (z + (z + z/255)/256)/256 - * = (z + z/256 + z/256/255) / 256 - * # only use 16bit uint operations, loose some precision, - * # result is floored. - * -> (z + z>>8)>>8 - * # add 0x80/255 = 0.5 to convert floor to round - * => (z+0x80 + (z+0x80)>>8 ) >> 8 - * ------ - * tested as equal to lround(z/255.0) for uint z in [0..0xfe02] - */ -#define PREMUL_ALPHA(x,a,b,z) { z = a * b + 0x80; x = (z + (z >> 8)) >> 8; } - for (int i = h; i; --i) { - const guint8 *gp = gdkpix; - unsigned char *cp = cpix; - const guint8* end = gp + 4*w; - guint z1, z2, z3; - while (gp < end) { -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - PREMUL_ALPHA(cp[0], gp[2], gp[3], z1); - PREMUL_ALPHA(cp[1], gp[1], gp[3], z2); - PREMUL_ALPHA(cp[2], gp[0], gp[3], z3); - cp[3] = gp[3]; -#else - PREMUL_ALPHA(cp[1], gp[0], gp[3], z1); - PREMUL_ALPHA(cp[2], gp[1], gp[3], z2); - PREMUL_ALPHA(cp[3], gp[2], gp[3], z3); - cp[0] = gp[3]; -#endif - gp += 4; - cp += 4; - } - gdkpix += stride; - cpix += cstride; - } -#undef PREMUL_ALPHA - } - cairo_surface_mark_dirty(cs); - return cs; -} diff --git a/icon.c b/icon.c index 3a0d49b7..31d32640 100644 --- a/icon.c +++ b/icon.c @@ -16,8 +16,7 @@ #ifdef HAVE_ICONS -#include -#include "cairo-pixbuf.h" +#include static bool validate_icon_name(const char* icon_name) { int icon_len = strlen(icon_name); @@ -40,29 +39,165 @@ static bool validate_icon_name(const char* icon_name) { return true; } -static GdkPixbuf *load_image(const char *path) { - if (strlen(path) == 0) { +static uint8_t to_premult(uint8_t x, uint8_t a) { + return (uint8_t)roundf((float)x * (float)a / 0xFF); +} + +/** + * Create a cairo surface from 8-bit unpacked RGB(A) pixel data, with straight + * alpha. + */ +static cairo_surface_t *create_surface_from_data(const uint8_t *src_pixels, + size_t width, size_t height, size_t src_stride, bool has_alpha) { + size_t src_bytes_per_pixel = has_alpha ? 4 : 3; + cairo_format_t format = has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24; + + cairo_surface_t *surface = cairo_image_surface_create(format, width, height); + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + fprintf(stderr, "Failed to create cairo surface\n"); + cairo_surface_destroy(surface); return NULL; } - GError *err = NULL; - GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(path, &err); - if (!pixbuf) { - fprintf(stderr, "Failed to load icon (%s)\n", err->message); - g_error_free(err); + + cairo_surface_flush(surface); + + uint8_t *dst_pixels = cairo_image_surface_get_data(surface); + int dst_stride = cairo_image_surface_get_stride(surface); + for (size_t y = 0; y < height; y++) { + for (size_t x = 0; x < width; x++) { + const uint8_t *src = &src_pixels[y * src_stride + x * src_bytes_per_pixel]; + uint8_t *dst = &dst_pixels[y * dst_stride + x * sizeof(uint32_t)]; + + uint8_t r = src[0]; + uint8_t g = src[1]; + uint8_t b = src[2]; + uint8_t a = has_alpha ? src[3] : 0xFF; + + // Convert from straight alpha to pre-multiplied alpha + r = to_premult(r, a); + g = to_premult(g, a); + b = to_premult(b, a); + + // Convert from unpacked RGBA to native-endian packed ARGB + uint32_t packed = 0; + packed |= (uint32_t)r << 16; + packed |= (uint32_t)g << 8; + packed |= b; + packed |= (uint32_t)a << 24; + + memcpy(dst, &packed, sizeof(packed)); + } + } + + cairo_surface_mark_dirty(surface); + + return surface; +} + +static cairo_surface_t *load_image(const char *path) { + if (strlen(path) == 0) { return NULL; } - return pixbuf; + + FILE *f = fopen(path, "r"); + if (f == NULL) { + fprintf(stderr, "Failed to open icon\n"); + goto error; + } + + struct klm_decoder *dec = klm_decoder_create_with_file(f); + if (dec == NULL) { + fprintf(stderr, "Failed to create icon decoder\n"); + goto error_file; + } + + const struct klm_decoder_info *info = klm_decoder_read_info(dec); + if (info == NULL) { + fprintf(stderr, "Failed to decode icon info\n"); + goto error_dec; + } + + enum klm_format format; + size_t bytes_per_pixel = 0; + bool has_alpha = false; + for (size_t i = 0; i < info->formats_len; i++) { + format = info->formats[i]; + if (format == KLM_FORMAT_R8G8B8) { + bytes_per_pixel = 3; + break; + } else if (format == KLM_FORMAT_R8G8B8A8) { + bytes_per_pixel = 4; + has_alpha = true; + break; + } + } + if (bytes_per_pixel == 0) { + fprintf(stderr, "Unsupported icon pixel format\n"); + goto error_dec; + } + + size_t stride = bytes_per_pixel * info->width; + size_t size = stride * info->height; + uint8_t *buffer = malloc(size); + if (buffer == NULL) { + perror("Failed to allocate buffer"); + goto error_buffer; + } + + bool to_srgb_gamma22 = info->color_primaries != 0 && info->color_transfer_function != 0; + struct klm_decoder_read_frame_options options = { + .format = format, + .buffer = buffer, + .size = size, + .stride = stride, + .color_primaries = to_srgb_gamma22 ? KLM_COLOR_PRIMARIES_SRGB : 0, + .color_transfer_function = to_srgb_gamma22 ? KLM_COLOR_TRANSFER_FUNCTION_GAMMA22 : 0, + }; + if (!klm_decoder_read_frame(dec, &options)) { + fprintf(stderr, "Failed to decode icon frame\n"); + goto error_buffer; + } + + cairo_surface_t *surface = create_surface_from_data(buffer, + info->width, info->height, stride, has_alpha); + if (surface == NULL) { + goto error_buffer; + } + + free(buffer); + klm_decoder_destroy(dec); + fclose(f); + return surface; + +error_buffer: + free(buffer); +error_dec: + klm_decoder_destroy(dec); +error_file: + fclose(f); +error: + fprintf(stderr, "Failed to load icon from %s\n", path); + return NULL; } -static GdkPixbuf *load_image_data(struct mako_image_data *image_data) { - GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(image_data->data, GDK_COLORSPACE_RGB, - image_data->has_alpha, image_data->bits_per_sample, image_data->width, - image_data->height, image_data->rowstride, NULL, NULL); - if (!pixbuf) { - fprintf(stderr, "Failed to load icon\n"); +static cairo_surface_t *load_image_data(struct mako_image_data *image_data) { + if (image_data->bits_per_sample != 8) { + fprintf(stderr, "Unsupported number of bits per sample\n"); return NULL; } - return pixbuf; + if ((image_data->has_alpha && image_data->channels != 4) || + (!image_data->has_alpha && image_data->channels != 3)) { + fprintf(stderr, "Unsupported number of channels\n"); + return NULL; + } + + cairo_surface_t *surface = create_surface_from_data(image_data->data, + image_data->width, image_data->height, image_data->rowstride, image_data->has_alpha); + if (surface == NULL) { + return NULL; + } + + return surface; } static double fit_to_square(int width, int height, int square_size) { @@ -251,40 +386,32 @@ static char *resolve_icon(struct mako_notification *notif) { } struct mako_icon *create_icon(struct mako_notification *notif) { - GdkPixbuf *image = NULL; + cairo_surface_t *surface = NULL; if (notif->image_data != NULL) { - image = load_image_data(notif->image_data); + surface = load_image_data(notif->image_data); } - if (image == NULL) { + if (surface == NULL) { char *path = resolve_icon(notif); if (path == NULL) { return NULL; } - image = load_image(path); + surface = load_image(path); free(path); - if (image == NULL) { + if (surface == NULL) { return NULL; } } - int image_width = gdk_pixbuf_get_width(image); - int image_height = gdk_pixbuf_get_height(image); + int image_width = cairo_image_surface_get_width(surface); + int image_height = cairo_image_surface_get_height(surface); struct mako_icon *icon = calloc(1, sizeof(struct mako_icon)); - icon->scale = fit_to_square( - image_width, image_height, notif->style.max_icon_size); + icon->scale = fit_to_square(image_width, image_height, notif->style.max_icon_size); icon->width = image_width * icon->scale; icon->height = image_height * icon->scale; - - icon->image = create_cairo_surface_from_gdk_pixbuf(image); - g_object_unref(image); - if (icon->image == NULL) { - free(icon); - return NULL; - } - + icon->image = surface; return icon; } #else diff --git a/include/cairo-pixbuf.h b/include/cairo-pixbuf.h deleted file mode 100644 index de826aa6..00000000 --- a/include/cairo-pixbuf.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef MAKO_CAIRO_PIXBUF_H -#define MAKO_CAIRO_PIXBUF_H - -#ifndef HAVE_ICONS -#error "gdk_pixbuf is required" -#endif - -#include -#include - -cairo_surface_t *create_cairo_surface_from_gdk_pixbuf(const GdkPixbuf *pixbuf); - -#endif diff --git a/meson.build b/meson.build index 3bca1c0d..1b4846e2 100644 --- a/meson.build +++ b/meson.build @@ -8,6 +8,7 @@ project( 'c_std=c11', 'warning_level=2', 'werror=true', + 'wrap_mode=nodownload', ], ) @@ -48,8 +49,8 @@ else endif add_project_arguments('-DHAVE_' + sdbus.name().to_upper(), language: 'c') -gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: get_option('icons')) -if gdk_pixbuf.found() +keulim = dependency('keulim', required: get_option('icons'), fallback: 'keulim') +if keulim.found() add_project_arguments('-DHAVE_ICONS', language: 'c') endif @@ -75,17 +76,13 @@ src_files = [ 'string-util.c', ] -if gdk_pixbuf.found() - src_files += 'cairo-pixbuf.c' -endif - executable( 'mako', files(src_files) + protocols_src, dependencies: [ cairo, epoll, - gdk_pixbuf, + keulim, sdbus, pango, pangocairo, @@ -120,6 +117,6 @@ subdir('doc') summary({ 'sd-bus provider': sdbus.name(), - 'Icons': gdk_pixbuf.found(), + 'Icons': keulim.found(), 'Man pages': scdoc.found(), }, bool_yn: true) diff --git a/subprojects/keulim.wrap b/subprojects/keulim.wrap new file mode 100644 index 00000000..ca8d2294 --- /dev/null +++ b/subprojects/keulim.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://gitlab.freedesktop.org/emersion/keulim.git +revision = HEAD